pairling 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -481,9 +481,6 @@ TERMINAL_SURFACE_V2_NONCE_SALT = os.urandom(16).hex()
481
481
  LAST_HUMAN_ACTIVITY_AT = 0.0
482
482
  DAEMON_STARTED_AT = _time.time()
483
483
  BOUND_HOST = ""
484
- POWER_STATE_PATH = Path(os.environ.get("COMPANION_POWER_STATE_PATH", "/var/run/pairling-power-state.json"))
485
- POWER_STATE_FALLBACK_PATH = Path(os.environ.get("COMPANION_POWER_STATE_FALLBACK_PATH", "/tmp/pairling-power-state.json"))
486
- POWER_STATE_STALE_SECONDS = 90
487
484
  DAEMON_VERSION = "2026-05-07"
488
485
 
489
486
  _sessions_health_lock = threading.Lock()
@@ -565,6 +562,7 @@ def _clean_terminal_display_text(text: str) -> str:
565
562
 
566
563
  _HEALTH_PROBE_CACHE_SECONDS = 30.0
567
564
  _HEALTH_PAYLOAD_CACHE_SECONDS = 5.0
565
+ REQUEST_READ_TIMEOUT_SECONDS = 15.0
568
566
  _health_probe_cache_lock = threading.Lock()
569
567
  _health_probe_cache: dict[str, tuple[float, object]] = {}
570
568
  _health_payload_cache_lock = threading.Lock()
@@ -606,7 +604,16 @@ _AUX_STREAM_ENDPOINTS = {
606
604
  "/invocations-stream",
607
605
  "/llm-route-stream",
608
606
  }
609
- _FAST_ENDPOINTS = {"/health", "/healthz", "/readyz", "/routez", "/power-state", "/manifest"}
607
+ _FAST_ENDPOINTS = {"/health", "/healthz", "/readyz", "/routez", "/manifest"}
608
+
609
+
610
+ def _is_orchestration_stream_path(path: str) -> bool:
611
+ prefix = f"{ORCHESTRATIONS_ROUTE}/"
612
+ suffix = "/stream"
613
+ if not path.startswith(prefix) or not path.endswith(suffix):
614
+ return False
615
+ inner = path[len(prefix):-len(suffix)]
616
+ return bool(inner) and "/" not in inner
610
617
 
611
618
 
612
619
  class _RuntimeAdmission:
@@ -936,12 +943,8 @@ def _pairdrop_gateway_provenance_ok(headers, client_address=None) -> bool:
936
943
  header_ok = str(getter("X-Pairling-Connect-Gateway", "") or "") == "pairling-connectd"
937
944
  if not header_ok:
938
945
  return False
939
- # The gateway header alone is spoofable, so in the default loopback-only bind
940
- # also require the loopback hop from connectd. Under PAIRLING_BIND_MODE=all a
941
- # direct-LAN client is non-loopback, so waive the loopback requirement there
942
- # to avoid breaking that path.
943
- if os.environ.get("PAIRLING_BIND_MODE", "loopback").strip().lower() == "all":
944
- return True
946
+ # The gateway header alone is spoofable. The trusted connectd hop is loopback,
947
+ # even when connectd received the original request over a tailnet route.
945
948
  return _loopback_client_address(client_address)
946
949
 
947
950
 
@@ -981,7 +984,7 @@ def _required_scopes_for_request(path: str, method: str) -> set[str]:
981
984
  return {"files:write"}
982
985
  if _is_pairdrop_upload_path(path):
983
986
  return {"files:write"}
984
- if path in {"/health", "/healthz", "/readyz", "/routez", "/power-state", "/health-stream", "/provider-status", "/status", "/aperture-cli/status", "/aperture-cli/providers", "/aperture-cli/launch-contexts"}:
987
+ if path in {"/health", "/healthz", "/readyz", "/routez", "/health-stream", "/provider-status", "/status", "/aperture-cli/status", "/aperture-cli/providers", "/aperture-cli/launch-contexts"}:
985
988
  return {"health:read"}
986
989
  if path == "/manifest":
987
990
  return {"manifest:read"}
@@ -1067,6 +1070,10 @@ def _funnel_origin_request(headers, client_address) -> bool:
1067
1070
  return str(getter("X-Pairling-Funnel-Origin", "") or "").strip() == "1"
1068
1071
 
1069
1072
 
1073
+ def _pair_claim_requires_app_attest(headers, client_address) -> bool:
1074
+ return _funnel_origin_request(headers, client_address) or not _loopback_client_address(client_address)
1075
+
1076
+
1070
1077
  def _connectd_peer_node_id(headers) -> str:
1071
1078
  getter = headers.get if hasattr(headers, "get") else lambda key, default=None: default
1072
1079
  value = str(getter("X-Pairling-Peer-Node", "") or "").strip()
@@ -1723,6 +1730,24 @@ def _agent_registry_heartbeat_by_claude_uuid(provider: str, claude_uuid: str, *,
1723
1730
  return False
1724
1731
 
1725
1732
 
1733
+ def _agent_registry_heartbeat_by_native_id(provider: str, native_id: str, *,
1734
+ terminal_tty: str = "", pid: int = 0) -> bool:
1735
+ if not provider or not native_id:
1736
+ return False
1737
+ try:
1738
+ with _agent_registry_conn() as conn:
1739
+ cur = conn.execute(
1740
+ "UPDATE agent_sessions SET last_heartbeat = ?, state = 'running', closed_at = NULL, "
1741
+ "terminal_tty = COALESCE(NULLIF(?, ''), terminal_tty), "
1742
+ "pid = COALESCE(NULLIF(?, 0), pid) "
1743
+ "WHERE provider = ? AND native_id = ?",
1744
+ (_time.time(), terminal_tty or "", int(pid or 0), provider, native_id),
1745
+ )
1746
+ return cur.rowcount > 0
1747
+ except Exception:
1748
+ return False
1749
+
1750
+
1726
1751
  def _agent_registry_mark_closed_by_claude_uuid(provider: str, claude_uuid: str) -> bool:
1727
1752
  """Tombstone keyed on claude_uuid (SessionEnd hook path). Idempotent —
1728
1753
  only rows without an existing closed_at are touched, like the PG
@@ -1741,6 +1766,21 @@ def _agent_registry_mark_closed_by_claude_uuid(provider: str, claude_uuid: str)
1741
1766
  return False
1742
1767
 
1743
1768
 
1769
+ def _agent_registry_mark_closed_by_native_id(provider: str, native_id: str) -> bool:
1770
+ if not provider or not native_id:
1771
+ return False
1772
+ try:
1773
+ with _agent_registry_conn() as conn:
1774
+ cur = conn.execute(
1775
+ "UPDATE agent_sessions SET closed_at = ?, state = 'terminated' "
1776
+ "WHERE provider = ? AND native_id = ? AND closed_at IS NULL",
1777
+ (_time.time(), provider, native_id),
1778
+ )
1779
+ return cur.rowcount > 0
1780
+ except Exception:
1781
+ return False
1782
+
1783
+
1744
1784
  def _registry_metadata_from_row(row: dict | None) -> dict:
1745
1785
  if not row:
1746
1786
  return {}
@@ -2007,7 +2047,7 @@ def _runtime_admission_for_path(path: str) -> _RuntimeAdmission:
2007
2047
  if _AUX_STREAM_ADMISSION_SEMAPHORE.acquire(blocking=False):
2008
2048
  return _RuntimeAdmission(_AUX_STREAM_ADMISSION_SEMAPHORE, True)
2009
2049
  return _RuntimeAdmission(None, False, "aux_stream_capacity_exceeded")
2010
- if path in _STREAM_ENDPOINTS:
2050
+ if path in _STREAM_ENDPOINTS or _is_orchestration_stream_path(path):
2011
2051
  if _STREAM_ADMISSION_SEMAPHORE.acquire(blocking=False):
2012
2052
  return _RuntimeAdmission(_STREAM_ADMISSION_SEMAPHORE, True)
2013
2053
  return _RuntimeAdmission(None, False, "stream_capacity_exceeded")
@@ -2151,19 +2191,6 @@ def _codex_terminal_tty_candidates(reg: dict | None) -> list[str]:
2151
2191
  return candidates
2152
2192
 
2153
2193
 
2154
- def _guardian_tailnet_ip(power_state: dict | None) -> str | None:
2155
- if not isinstance(power_state, dict):
2156
- return None
2157
- for section_name in ("network", "facts"):
2158
- section = power_state.get(section_name)
2159
- if not isinstance(section, dict):
2160
- continue
2161
- ip = str(section.get("tailscale_ip") or "").strip()
2162
- if ip.startswith("100."):
2163
- return ip
2164
- return None
2165
-
2166
-
2167
2194
  def _probe_tailnet_ip() -> str | None:
2168
2195
  ok, out, _ = _run_text(["tailscale", "ip", "-4"], timeout=3)
2169
2196
  if not ok:
@@ -2175,29 +2202,10 @@ def _probe_tailnet_ip() -> str | None:
2175
2202
  return None
2176
2203
 
2177
2204
 
2178
- def _tailnet_ip(power_state: dict | None = None) -> str | None:
2179
- guardian_ip = _guardian_tailnet_ip(power_state)
2180
- if guardian_ip:
2181
- return guardian_ip
2205
+ def _tailnet_ip() -> str | None:
2182
2206
  return _cached_probe("tailnet_ip", _HEALTH_PROBE_CACHE_SECONDS, _probe_tailnet_ip)
2183
2207
 
2184
2208
 
2185
- def _guardian_lan_ips(power_state: dict | None) -> list[str]:
2186
- if not isinstance(power_state, dict):
2187
- return []
2188
- network = power_state.get("network")
2189
- if not isinstance(network, dict):
2190
- return []
2191
- ips: list[str] = []
2192
- for item in network.get("lan_ips") or []:
2193
- ip = str(item or "").strip()
2194
- if not ip or ip.startswith(("127.", "169.254.", "100.")):
2195
- continue
2196
- if ip not in ips:
2197
- ips.append(ip)
2198
- return ips
2199
-
2200
-
2201
2209
  def _probe_lan_ips() -> list[str]:
2202
2210
  ok, out, _ = _run_text(["/sbin/ifconfig"], timeout=3)
2203
2211
  if not ok:
@@ -2212,27 +2220,10 @@ def _probe_lan_ips() -> list[str]:
2212
2220
  return ips
2213
2221
 
2214
2222
 
2215
- def _lan_ips(power_state: dict | None = None) -> list[str]:
2216
- guardian_ips = _guardian_lan_ips(power_state)
2217
- if guardian_ips:
2218
- return guardian_ips
2223
+ def _lan_ips() -> list[str]:
2219
2224
  return _cached_probe("lan_ips", _HEALTH_PROBE_CACHE_SECONDS, _probe_lan_ips)
2220
2225
 
2221
2226
 
2222
- def _guardian_listener_entries(power_state: dict | None) -> list[str]:
2223
- if not isinstance(power_state, dict):
2224
- return []
2225
- daemon = power_state.get("daemon")
2226
- if not isinstance(daemon, dict):
2227
- return []
2228
- entries: list[str] = []
2229
- for item in daemon.get("listen") or []:
2230
- entry = str(item or "").strip()
2231
- if entry and entry not in entries:
2232
- entries.append(entry)
2233
- return entries
2234
-
2235
-
2236
2227
  def _probe_listener_entries() -> list[str]:
2237
2228
  host = BOUND_HOST or os.environ.get("PAIRLING_BOUND_HOST", "")
2238
2229
  entries: list[str] = []
@@ -2249,153 +2240,10 @@ def _probe_listener_entries() -> list[str]:
2249
2240
  return entries
2250
2241
 
2251
2242
 
2252
- def _listener_entries(power_state: dict | None = None) -> list[str]:
2253
- guardian_entries = _guardian_listener_entries(power_state)
2254
- if guardian_entries:
2255
- return guardian_entries
2243
+ def _listener_entries() -> list[str]:
2256
2244
  return _cached_probe("listener_entries", _HEALTH_PROBE_CACHE_SECONDS, _probe_listener_entries)
2257
2245
 
2258
2246
 
2259
- def _read_guardian_state() -> tuple[dict | None, str | None, float | None, str | None]:
2260
- for path in (POWER_STATE_PATH, POWER_STATE_FALLBACK_PATH):
2261
- try:
2262
- if not path.exists():
2263
- continue
2264
- state = json.loads(path.read_text())
2265
- generated = float(state.get("generated_at") or state.get("ts") or 0)
2266
- age = max(0.0, _time.time() - generated) if generated else None
2267
- return state, str(path), age, None
2268
- except Exception as exc:
2269
- return None, str(path), None, f"{type(exc).__name__}: {exc}"
2270
- return None, None, None, "guardian state missing"
2271
-
2272
-
2273
- def _coordinator_from_guardian(state: dict | None, age: float | None, error: str | None) -> dict:
2274
- if not state:
2275
- return {
2276
- "role": "primary_coordinator",
2277
- "posture": "unknown",
2278
- "severity": "unknown",
2279
- "summary": error or "Guardian state is unavailable",
2280
- "stale": True,
2281
- }
2282
- if isinstance(state.get("posture"), dict):
2283
- posture = state.get("posture") or {}
2284
- status = posture.get("status") or "unknown"
2285
- severity = posture.get("severity") or ("ok" if status == "ready" else status)
2286
- summary = posture.get("summary") or state.get("summary") or "Coordinator posture is unknown"
2287
- else:
2288
- status = state.get("posture") or "unknown"
2289
- severity = state.get("severity") or ("ok" if status == "ready" else status)
2290
- summary = state.get("summary") or "Coordinator posture is unknown"
2291
- stale = age is None or age > POWER_STATE_STALE_SECONDS
2292
- if stale:
2293
- status = "unknown"
2294
- severity = "unknown"
2295
- summary = f"Guardian sample is stale ({int(age or 0)}s old)"
2296
- return {
2297
- "role": (state.get("host") or {}).get("role") if isinstance(state.get("host"), dict) else "primary_coordinator",
2298
- "host": (state.get("host") or {}).get("name") if isinstance(state.get("host"), dict) else (state.get("host") or DEFAULT_COORDINATOR_HOST),
2299
- "posture": status,
2300
- "severity": severity,
2301
- "summary": summary,
2302
- "stale": stale,
2303
- "sample_age_seconds": age,
2304
- }
2305
-
2306
-
2307
- def _normalize_guardian_state(state: dict | None) -> dict | None:
2308
- if not state:
2309
- return None
2310
- if isinstance(state.get("posture"), dict) and any(isinstance(state.get(key), dict) for key in ("host", "network", "daemon")):
2311
- normalized = dict(state)
2312
- host = normalized.get("host")
2313
- if not isinstance(host, dict):
2314
- normalized["host"] = {
2315
- "name": str(host or DEFAULT_COORDINATOR_HOST),
2316
- "role": "primary_coordinator",
2317
- }
2318
- return normalized
2319
-
2320
- facts = state.get("facts") if isinstance(state.get("facts"), dict) else {}
2321
- checks_in = state.get("checks") if isinstance(state.get("checks"), list) else []
2322
- checks: dict[str, str] = {}
2323
- warnings: list[str] = []
2324
- for item in checks_in:
2325
- if not isinstance(item, dict):
2326
- continue
2327
- ident = str(item.get("id") or "check")
2328
- ok = bool(item.get("ok"))
2329
- message = str(item.get("message") or ("ok" if ok else "failed"))
2330
- checks[ident] = "ok" if ok else message
2331
- if not ok:
2332
- warnings.append(message)
2333
-
2334
- sleep_minutes = facts.get("sleep_minutes")
2335
- disk_sleep = facts.get("disk_sleep_minutes")
2336
- thermal_speed = facts.get("thermal_cpu_speed_limit")
2337
- thermal_scheduler = facts.get("thermal_cpu_scheduler_limit")
2338
- thermal_state = "nominal"
2339
- if isinstance(thermal_speed, (int, float)) and thermal_speed < 80:
2340
- thermal_state = "warning"
2341
- if isinstance(thermal_scheduler, (int, float)) and thermal_scheduler < 80:
2342
- thermal_state = "warning"
2343
-
2344
- host_name = state.get("host") if isinstance(state.get("host"), str) else DEFAULT_COORDINATOR_HOST
2345
- return {
2346
- "schema_version": state.get("schema_version") or 1,
2347
- "generated_at": state.get("generated_at") or state.get("ts") or _time.time(),
2348
- "host": {
2349
- "name": host_name or DEFAULT_COORDINATOR_HOST,
2350
- "role": "primary_coordinator",
2351
- },
2352
- "posture": {
2353
- "status": state.get("posture") or "unknown",
2354
- "severity": state.get("severity") or "unknown",
2355
- "summary": state.get("summary") or "Coordinator posture is unknown",
2356
- },
2357
- "power": {
2358
- "ac_power": facts.get("ac_power"),
2359
- "battery_percent": facts.get("battery_percent"),
2360
- "low_power_mode": facts.get("low_power_mode"),
2361
- "system_sleep_disabled": sleep_minutes == 0 if sleep_minutes is not None else None,
2362
- "display_sleep_minutes": facts.get("display_sleep_minutes"),
2363
- "disk_sleep_disabled": disk_sleep == 0 if disk_sleep is not None else None,
2364
- "caffeinate_pid": facts.get("caffeinate_pid"),
2365
- "prevent_system_sleep": facts.get("prevent_system_sleep"),
2366
- "prevent_idle_system_sleep": facts.get("prevent_user_idle_system_sleep"),
2367
- "prevent_display_sleep": facts.get("prevent_user_idle_display_sleep"),
2368
- },
2369
- "lid": {
2370
- "closed": facts.get("lid_closed"),
2371
- "apple_clamshell_causes_sleep": facts.get("clamshell_causes_sleep"),
2372
- "supported_posture": facts.get("lid_closed") is False,
2373
- },
2374
- "thermal": {
2375
- "state": thermal_state,
2376
- "cpu_speed_limit": thermal_speed,
2377
- "cpu_scheduler_limit": thermal_scheduler,
2378
- },
2379
- "network": {
2380
- "tailscale_installed": facts.get("tailscale_ip") is not None,
2381
- "tailscale_variant": "standalone",
2382
- "tailscale_ip": facts.get("tailscale_ip"),
2383
- "tailscale_status": "ok" if facts.get("tailscale_ip") else "missing",
2384
- "default_interface": None,
2385
- "lan_ips": [],
2386
- },
2387
- "daemon": {
2388
- "pairling_pid": os.getpid(),
2389
- "listen": (listener_entries := _listener_entries()),
2390
- "reachable_local": bool(listener_entries),
2391
- "reachable_tailnet": facts.get("daemon_reachable"),
2392
- },
2393
- "warnings": warnings,
2394
- "checks": checks,
2395
- "raw_schema": "legacy_flat_guardian",
2396
- }
2397
-
2398
-
2399
2247
  def _pairling_connect_health() -> dict:
2400
2248
  """Cached snapshot of the Pairling Connect (connectd) axis for health
2401
2249
  surfaces: {"ready": bool, "summary": dict | None, "routes": list}.
@@ -2449,38 +2297,7 @@ def _coordinator_from_pairling_connect(connect: dict) -> dict:
2449
2297
  }
2450
2298
 
2451
2299
 
2452
- # Guardian checks whose failure is fully compensated by a ready Pairling
2453
- # Connect route: both only measure the standalone-Tailscale axis.
2454
- _TAILNET_AXIS_CHECK_IDS = {"tailscale_ip", "daemon_reachable"}
2455
-
2456
-
2457
- def _apply_pairling_connect_posture(coordinator: dict, power_state: dict | None, connect: dict) -> dict:
2458
- """Downgrade tailnet-axis criticality when the Pairling Connect route is
2459
- ready. The guardian historically measured only standalone Tailscale; a
2460
- healthy embedded connectd route serves phones regardless, so its absence
2461
- alone must not mark the coordinator unsafe. Any other failing check
2462
- keeps the original posture untouched."""
2463
- if not connect.get("ready"):
2464
- return coordinator
2465
- if coordinator.get("posture") != "unsafe":
2466
- return coordinator
2467
- checks = (power_state or {}).get("checks")
2468
- if not isinstance(checks, dict) or not checks:
2469
- return coordinator
2470
- failing = {cid for cid, msg in checks.items() if msg != "ok"}
2471
- if not failing or not failing.issubset(_TAILNET_AXIS_CHECK_IDS):
2472
- return coordinator
2473
- adjusted = dict(coordinator)
2474
- adjusted["posture"] = "warning"
2475
- adjusted["severity"] = "warning"
2476
- adjusted["summary"] = (
2477
- "Pairling Connect tailnet route is ready; standalone Tailscale is offline."
2478
- )
2479
- adjusted["tailnet_axis"] = "pairling_connect"
2480
- return adjusted
2481
-
2482
-
2483
- def _health_routes(coordinator: dict, power_state: dict | None = None) -> list[dict]:
2300
+ def _health_routes(coordinator: dict) -> list[dict]:
2484
2301
  now = _time.time()
2485
2302
  routes: list[dict] = []
2486
2303
  connect = _pairling_connect_health()
@@ -2498,7 +2315,7 @@ def _health_routes(coordinator: dict, power_state: dict | None = None) -> list[d
2498
2315
  "source": str(route.get("source") or "pairling_connectd"),
2499
2316
  "id": route.get("id"),
2500
2317
  })
2501
- tailnet = _tailnet_ip(power_state)
2318
+ tailnet = _tailnet_ip()
2502
2319
  if tailnet:
2503
2320
  ok = coordinator.get("posture") in ("ready", "warning")
2504
2321
  routes.append({
@@ -2508,7 +2325,7 @@ def _health_routes(coordinator: dict, power_state: dict | None = None) -> list[d
2508
2325
  "score": 100 if ok else 40,
2509
2326
  "last_ok_at": now if ok else None,
2510
2327
  })
2511
- for ip in _lan_ips(power_state)[:2]:
2328
+ for ip in _lan_ips()[:2]:
2512
2329
  routes.append({
2513
2330
  "kind": "lan",
2514
2331
  "base_url": f"http://{ip}:{PORT}",
@@ -2529,8 +2346,8 @@ def _health_routes(coordinator: dict, power_state: dict | None = None) -> list[d
2529
2346
  return routes
2530
2347
 
2531
2348
 
2532
- def _daemon_snapshot(power_state: dict | None = None) -> dict:
2533
- entries = _listener_entries(power_state)
2349
+ def _daemon_snapshot() -> dict:
2350
+ entries = _listener_entries()
2534
2351
  return {
2535
2352
  "name": "pairlingd",
2536
2353
  "pid": os.getpid(),
@@ -2603,14 +2420,13 @@ def _routez_payload(auth_result=None) -> dict:
2603
2420
  }
2604
2421
 
2605
2422
 
2606
- def _health_payload(full_power: bool = False, authenticated: bool = False, auth_result=None) -> dict:
2423
+ def _health_payload(authenticated: bool = False, auth_result=None) -> dict:
2607
2424
  connect = _pairling_connect_health()
2608
- power_state = None
2609
2425
  coordinator = _coordinator_from_pairling_connect(connect)
2610
2426
  if connect.get("summary") is not None:
2611
2427
  coordinator = dict(coordinator)
2612
2428
  coordinator["pairling_connect"] = connect["summary"]
2613
- routes = _health_routes(coordinator, power_state)
2429
+ routes = _health_routes(coordinator)
2614
2430
  ok = coordinator.get("posture") in ("ready", "warning")
2615
2431
  runtime_info = _runtime_info_snapshot()
2616
2432
  public_runtime = _public_runtime_info(runtime_info) if _public_runtime_info else runtime_info
@@ -2625,7 +2441,7 @@ def _health_payload(full_power: bool = False, authenticated: bool = False, auth_
2625
2441
  "required": True,
2626
2442
  "legacy_global_token": False,
2627
2443
  },
2628
- "daemon": _daemon_snapshot(power_state),
2444
+ "daemon": _daemon_snapshot(),
2629
2445
  "coordinator": coordinator,
2630
2446
  }
2631
2447
  if authenticated:
@@ -2649,16 +2465,16 @@ def _health_payload(full_power: bool = False, authenticated: bool = False, auth_
2649
2465
  return payload
2650
2466
 
2651
2467
 
2652
- def _cached_health_payload(full_power: bool = False, authenticated: bool = False, auth_result=None) -> dict:
2468
+ def _cached_health_payload(authenticated: bool = False, auth_result=None) -> dict:
2653
2469
  device_id = str(getattr(auth_result, "device_id", "") or "")
2654
2470
  install_id = str(getattr(auth_result, "install_id", "") or "")
2655
- key = (bool(full_power), bool(authenticated), device_id, install_id)
2471
+ key = (bool(authenticated), device_id, install_id)
2656
2472
  now = _time.time()
2657
2473
  with _health_payload_cache_lock:
2658
2474
  cached = _health_payload_cache.get(key)
2659
2475
  if cached is not None and now - cached[0] < _HEALTH_PAYLOAD_CACHE_SECONDS:
2660
2476
  return _copy_cache_value(cached[1])
2661
- payload = _health_payload(full_power=full_power, authenticated=authenticated, auth_result=auth_result)
2477
+ payload = _health_payload(authenticated=authenticated, auth_result=auth_result)
2662
2478
  with _health_payload_cache_lock:
2663
2479
  _health_payload_cache[key] = (now, _copy_cache_value(payload))
2664
2480
  return _copy_cache_value(payload)
@@ -2694,38 +2510,24 @@ def _health_diff_digest(payload: dict) -> str:
2694
2510
  mirror = stable.get("mirror")
2695
2511
  if isinstance(mirror, dict):
2696
2512
  mirror.pop("updated_at", None)
2697
- stable.pop("guardian_sample_age_seconds", None)
2698
2513
  return hashlib.sha256(json.dumps(stable, sort_keys=True).encode()).hexdigest()
2699
2514
 
2700
2515
 
2701
2516
  def _orchestration_preflight_from_health(health: dict) -> tuple[dict, dict]:
2702
- power_state = health.get("power_state") if isinstance(health.get("power_state"), dict) else None
2703
- if power_state is None:
2704
- power_state = {}
2705
2517
  coordinator = health.get("coordinator") or {}
2706
2518
  route = (health.get("routes") or [{}])[0]
2707
2519
  runtime_info = health.get("runtime") if isinstance(health.get("runtime"), dict) else {}
2708
- power = power_state.get("power") if isinstance(power_state.get("power"), dict) else {}
2709
- lid = power_state.get("lid") if isinstance(power_state.get("lid"), dict) else {}
2710
- network = power_state.get("network") if isinstance(power_state.get("network"), dict) else {}
2711
- thermal = power_state.get("thermal") if isinstance(power_state.get("thermal"), dict) else {}
2712
- facts = power_state.get("facts") if isinstance(power_state.get("facts"), dict) else {}
2713
2520
  preflight = {
2714
2521
  "posture": coordinator.get("posture") or "unknown",
2715
- "warnings": power_state.get("warnings") or [],
2522
+ "warnings": [],
2716
2523
  "route": route.get("kind") or "unknown",
2717
2524
  "route_base": route.get("base_url"),
2718
2525
  "checked_at": health.get("ts"),
2719
- "tailscale_ip": network.get("tailscale_ip") or facts.get("tailscale_ip"),
2720
- "lid_closed": lid.get("closed") if "closed" in lid else facts.get("lid_closed"),
2721
- "ac_power": power.get("ac_power") if "ac_power" in power else facts.get("ac_power"),
2722
- "low_power_mode": power.get("low_power_mode") if "low_power_mode" in power else facts.get("low_power_mode"),
2723
- "thermal": thermal.get("state") or (
2724
- "throttled" if (
2725
- (facts.get("thermal_cpu_speed_limit") is not None and facts.get("thermal_cpu_speed_limit") < 80) or
2726
- (facts.get("thermal_cpu_scheduler_limit") is not None and facts.get("thermal_cpu_scheduler_limit") < 80)
2727
- ) else "normal"
2728
- ),
2526
+ "tailscale_ip": None,
2527
+ "lid_closed": None,
2528
+ "ac_power": None,
2529
+ "low_power_mode": None,
2530
+ "thermal": "unknown",
2729
2531
  "runtime_version": runtime_info.get("runtime_version"),
2730
2532
  "runtime_source_revision": runtime_info.get("source_revision"),
2731
2533
  "runtime_contract_version": runtime_info.get("contract_version") or RUNTIME_CONTRACT_VERSION,
@@ -7542,28 +7344,37 @@ class ClientDisconnected(Exception):
7542
7344
 
7543
7345
 
7544
7346
  class Handler(BaseHTTPRequestHandler):
7347
+ timeout = REQUEST_READ_TIMEOUT_SECONDS
7545
7348
 
7546
7349
  def do_GET(self):
7547
7350
  try:
7548
7351
  return self._dispatch()
7352
+ except socket.timeout:
7353
+ self._send_json({"ok": False, "error": {"code": "request_timeout"}}, status=408)
7549
7354
  except ClientDisconnected:
7550
7355
  return
7551
7356
 
7552
7357
  def do_POST(self):
7553
7358
  try:
7554
7359
  return self._dispatch()
7360
+ except socket.timeout:
7361
+ self._send_json({"ok": False, "error": {"code": "request_timeout"}}, status=408)
7555
7362
  except ClientDisconnected:
7556
7363
  return
7557
7364
 
7558
7365
  def do_PUT(self):
7559
7366
  try:
7560
7367
  return self._dispatch()
7368
+ except socket.timeout:
7369
+ self._send_json({"ok": False, "error": {"code": "request_timeout"}}, status=408)
7561
7370
  except ClientDisconnected:
7562
7371
  return
7563
7372
 
7564
7373
  def do_DELETE(self):
7565
7374
  try:
7566
7375
  return self._dispatch()
7376
+ except socket.timeout:
7377
+ self._send_json({"ok": False, "error": {"code": "request_timeout"}}, status=408)
7567
7378
  except ClientDisconnected:
7568
7379
  return
7569
7380
 
@@ -7817,7 +7628,9 @@ class Handler(BaseHTTPRequestHandler):
7817
7628
  elif u.path == "/manifest":
7818
7629
  self._handle_manifest(q)
7819
7630
  elif u.path == "/pair/start":
7820
- if _funnel_origin_request(self.headers, self.client_address):
7631
+ if not _loopback_client_address(self.client_address):
7632
+ self._send_json({"ok": False, "error": {"code": "pair_start_loopback_required", "message": "pair start is only available from loopback"}}, status=403)
7633
+ elif _funnel_origin_request(self.headers, self.client_address):
7821
7634
  # Belt-and-suspenders: /pair/start returns the 192-bit secret
7822
7635
  # in plaintext and must never be served to a funnel-origin
7823
7636
  # request, even if connectd's allowlist ever drifted.
@@ -7840,8 +7653,6 @@ class Handler(BaseHTTPRequestHandler):
7840
7653
  self._handle_pair_bind_node(q)
7841
7654
  elif u.path == "/healthz":
7842
7655
  self._handle_healthz(q)
7843
- elif u.path == "/power-state":
7844
- self._handle_power_state(q)
7845
7656
  elif u.path == "/health-stream":
7846
7657
  self._handle_health_stream(q)
7847
7658
  elif u.path == "/open":
@@ -8075,17 +7886,24 @@ class Handler(BaseHTTPRequestHandler):
8075
7886
  uuid = str(payload.get("claude_uuid") or "").strip()
8076
7887
  return uuid if self._CLAUDE_UUID_RE.match(uuid) else ""
8077
7888
 
7889
+ def _internal_provider(self, payload: dict) -> str:
7890
+ provider = str(payload.get("provider") or "claude").strip().lower()
7891
+ return provider if provider in {"claude", "codex"} else "claude"
7892
+
8078
7893
  def _internal_terminal_tty(self, payload: dict) -> str:
8079
7894
  tty = str(payload.get("terminal_tty") or "").strip()
8080
7895
  return tty if self._INTERNAL_TTY_RE.match(tty) else ""
8081
7896
 
8082
- def _internal_claude_pid(self, payload: dict) -> int:
7897
+ def _internal_pid(self, payload: dict) -> int:
8083
7898
  try:
8084
- pid = int(payload.get("claude_pid") or 0)
7899
+ pid = int(payload.get("pid") or payload.get("claude_pid") or payload.get("codex_pid") or 0)
8085
7900
  except (TypeError, ValueError):
8086
7901
  return 0
8087
7902
  return pid if 0 < pid < 10 ** 8 else 0
8088
7903
 
7904
+ def _internal_claude_pid(self, payload: dict) -> int:
7905
+ return self._internal_pid(payload)
7906
+
8089
7907
  def _read_internal_json(self) -> dict | None:
8090
7908
  if self.command != "POST":
8091
7909
  self.send_error(405, "POST required")
@@ -8104,6 +7922,7 @@ class Handler(BaseHTTPRequestHandler):
8104
7922
  payload = self._read_internal_json()
8105
7923
  if payload is None:
8106
7924
  return
7925
+ provider = self._internal_provider(payload)
8107
7926
  session_id = str(payload.get("id") or "").strip()
8108
7927
  project = str(payload.get("project") or "").strip()
8109
7928
  if not _safe_session_id(session_id) or not project:
@@ -8114,17 +7933,18 @@ class Handler(BaseHTTPRequestHandler):
8114
7933
  return
8115
7934
  claude_uuid = self._internal_claude_uuid(payload)
8116
7935
  ok = _agent_registry_upsert(
8117
- "claude",
7936
+ provider,
8118
7937
  session_id,
8119
7938
  project,
8120
- pid=self._internal_claude_pid(payload),
7939
+ pid=self._internal_pid(payload),
8121
7940
  terminal_tty=self._internal_terminal_tty(payload),
8122
- claude_uuid=claude_uuid,
7941
+ claude_uuid=claude_uuid if provider == "claude" else "",
8123
7942
  working_on=str(payload.get("working_on") or "")[:500],
7943
+ metadata={"registered_by": "internal_session_register"},
8124
7944
  )
8125
7945
  # Mirrors the PG pg_notify('session_ready'): only fire once the row
8126
7946
  # carries a claude_uuid — that is what /turn-state-stream waits on.
8127
- if ok and claude_uuid:
7947
+ if provider == "claude" and ok and claude_uuid:
8128
7948
  _signal_session_ready(session_id)
8129
7949
  self._send_json({"ok": bool(ok)})
8130
7950
 
@@ -8132,6 +7952,23 @@ class Handler(BaseHTTPRequestHandler):
8132
7952
  payload = self._read_internal_json()
8133
7953
  if payload is None:
8134
7954
  return
7955
+ provider = self._internal_provider(payload)
7956
+ if provider == "codex":
7957
+ session_id = str(payload.get("id") or "").strip()
7958
+ if not _safe_session_id(session_id):
7959
+ self._send_json({
7960
+ "ok": False,
7961
+ "error": {"code": "bad_request", "message": "id required"},
7962
+ }, status=400)
7963
+ return
7964
+ ok = _agent_registry_heartbeat_by_native_id(
7965
+ "codex",
7966
+ session_id,
7967
+ terminal_tty=self._internal_terminal_tty(payload),
7968
+ pid=self._internal_pid(payload),
7969
+ )
7970
+ self._send_json({"ok": bool(ok)})
7971
+ return
8135
7972
  claude_uuid = self._internal_claude_uuid(payload)
8136
7973
  if not claude_uuid:
8137
7974
  self._send_json({
@@ -8143,7 +7980,7 @@ class Handler(BaseHTTPRequestHandler):
8143
7980
  "claude",
8144
7981
  claude_uuid,
8145
7982
  terminal_tty=self._internal_terminal_tty(payload),
8146
- pid=self._internal_claude_pid(payload),
7983
+ pid=self._internal_pid(payload),
8147
7984
  )
8148
7985
  # ok=false simply means no row matched — same as the PG UPDATE no-op.
8149
7986
  self._send_json({"ok": bool(ok)})
@@ -8152,6 +7989,18 @@ class Handler(BaseHTTPRequestHandler):
8152
7989
  payload = self._read_internal_json()
8153
7990
  if payload is None:
8154
7991
  return
7992
+ provider = self._internal_provider(payload)
7993
+ if provider == "codex":
7994
+ session_id = str(payload.get("id") or "").strip()
7995
+ if not _safe_session_id(session_id):
7996
+ self._send_json({
7997
+ "ok": False,
7998
+ "error": {"code": "bad_request", "message": "id required"},
7999
+ }, status=400)
8000
+ return
8001
+ closed = _agent_registry_mark_closed_by_native_id("codex", session_id)
8002
+ self._send_json({"ok": True, "closed": bool(closed)})
8003
+ return
8155
8004
  claude_uuid = self._internal_claude_uuid(payload)
8156
8005
  if not claude_uuid:
8157
8006
  self._send_json({
@@ -8164,20 +8013,24 @@ class Handler(BaseHTTPRequestHandler):
8164
8013
 
8165
8014
  def _handle_internal_active_sessions(self, q):
8166
8015
  project = q.get("project", [""])[0].strip()
8016
+ provider_filter = str(q.get("provider", ["claude"])[0] or "claude").strip().lower()
8017
+ providers = ["claude", "codex"] if provider_filter == "all" else [provider_filter if provider_filter in {"claude", "codex"} else "claude"]
8167
8018
  cutoff = _time.time() - 300
8168
8019
  items = []
8169
- for row in _agent_registry_live("claude"):
8170
- if float(row.get("last_heartbeat") or 0) < cutoff:
8171
- continue
8172
- if project and row.get("project") != project:
8173
- continue
8174
- items.append({
8175
- "id": row.get("native_id"),
8176
- "project": row.get("project"),
8177
- "working_on": row.get("working_on") or None,
8178
- "started_at": float(row.get("started_at") or 0),
8179
- "last_heartbeat": float(row.get("last_heartbeat") or 0),
8180
- })
8020
+ for provider in providers:
8021
+ for row in _agent_registry_live(provider):
8022
+ if float(row.get("last_heartbeat") or 0) < cutoff:
8023
+ continue
8024
+ if project and row.get("project") != project:
8025
+ continue
8026
+ items.append({
8027
+ "id": row.get("native_id"),
8028
+ "provider": provider,
8029
+ "project": row.get("project"),
8030
+ "working_on": row.get("working_on") or None,
8031
+ "started_at": float(row.get("started_at") or 0),
8032
+ "last_heartbeat": float(row.get("last_heartbeat") or 0),
8033
+ })
8181
8034
  items.sort(key=lambda r: r["started_at"], reverse=True)
8182
8035
  self._send_json({"ok": True, "count": len(items), "sessions": items})
8183
8036
 
@@ -8233,17 +8086,15 @@ class Handler(BaseHTTPRequestHandler):
8233
8086
  subprocess.run(["open", "-a", SUBLIME_APP, path], check=False)
8234
8087
  self._send_text(200, b"ok\n")
8235
8088
 
8236
- # ----- /healthz + /power-state + /health-stream: coordinator health -----
8089
+ # ----- /healthz + /health-stream: coordinator health -----
8237
8090
  def _handle_health(self, q):
8238
8091
  self._send_json(_cached_health_payload(
8239
- full_power=False,
8240
8092
  authenticated=self.pairling_auth is not None,
8241
8093
  auth_result=self.pairling_auth,
8242
8094
  ))
8243
8095
 
8244
8096
  def _handle_healthz(self, q):
8245
8097
  self._send_json(_cached_health_payload(
8246
- full_power=False,
8247
8098
  authenticated=self.pairling_auth is not None,
8248
8099
  auth_result=self.pairling_auth,
8249
8100
  ))
@@ -8563,6 +8414,7 @@ class Handler(BaseHTTPRequestHandler):
8563
8414
  relay_device_id=payload.get("relay_device_id"),
8564
8415
  relay_required=bool(relay_claims_required and relay_claims_required()),
8565
8416
  relay_claim_verifier=RELAY_CLAIM_VERIFIER,
8417
+ require_direct_attest=_pair_claim_requires_app_attest(self.headers, self.client_address),
8566
8418
  )
8567
8419
  if PAIRING_ADVERTISER is not None:
8568
8420
  PAIRING_ADVERTISER.stop()
@@ -8620,6 +8472,7 @@ class Handler(BaseHTTPRequestHandler):
8620
8472
  payload = self._read_json_object()
8621
8473
  host_chain = self._pairing_host_chain()
8622
8474
  funnel_origin = _funnel_origin_request(self.headers, self.client_address)
8475
+ require_direct_attest = _pair_claim_requires_app_attest(self.headers, self.client_address)
8623
8476
  claim, k_token, aad, mac_confirm = PAIRING_STORE.psk_claim_pair(
8624
8477
  pair_id=str(payload.get("pair_id") or ""),
8625
8478
  b_pub_b64=str(payload.get("b_pub") or ""),
@@ -8635,6 +8488,7 @@ class Handler(BaseHTTPRequestHandler):
8635
8488
  relay_required=bool(relay_claims_required and relay_claims_required()),
8636
8489
  relay_claim_verifier=RELAY_CLAIM_VERIFIER,
8637
8490
  funnel_origin=funnel_origin,
8491
+ require_direct_attest=require_direct_attest,
8638
8492
  )
8639
8493
  nonce, enc_token = PAIRING_STORE.seal_psk_token(k_token, claim.device.token, aad)
8640
8494
  # Seal proof_secret (the request-proof HMAC key) under K_token for
@@ -8800,13 +8654,6 @@ class Handler(BaseHTTPRequestHandler):
8800
8654
  )
8801
8655
  self._send_json({"ok": True, "tailnet_node_id_bound": bound})
8802
8656
 
8803
- def _handle_power_state(self, q):
8804
- self._send_json(_cached_health_payload(
8805
- full_power=True,
8806
- authenticated=self.pairling_auth is not None,
8807
- auth_result=self.pairling_auth,
8808
- ))
8809
-
8810
8657
  def _handle_mirror_status(self, q):
8811
8658
  project = q.get("project", [None])[0]
8812
8659
  args = ["status"]
@@ -8922,7 +8769,6 @@ class Handler(BaseHTTPRequestHandler):
8922
8769
  deadline = _time.time() + 600
8923
8770
  while _time.time() < deadline:
8924
8771
  payload = _cached_health_payload(
8925
- full_power=False,
8926
8772
  authenticated=self.pairling_auth is not None,
8927
8773
  auth_result=self.pairling_auth,
8928
8774
  )
@@ -11756,15 +11602,22 @@ class Handler(BaseHTTPRequestHandler):
11756
11602
  project_basename = os.path.basename(project.rstrip("/")) or project
11757
11603
  result = self._applescript_inject(project_basename, text)
11758
11604
 
11605
+ status = 200
11759
11606
  if not result.get("ok"):
11760
11607
  # Fall back to the queue-file path so nothing is lost
11761
11608
  queue_file = QUEUE_DIR / f"{session_id}.txt"
11762
11609
  with open(queue_file, "a") as f:
11763
11610
  f.write(text + "\n")
11611
+ status = 202
11764
11612
  body = json.dumps({
11765
- "ok": True, "injected": False, "queued": True,
11613
+ "ok": False, "injected": False, "queued": True,
11614
+ "state": "queued_not_injected",
11766
11615
  "fallback_reason": result.get("reason", "unknown"),
11767
11616
  "window_match": project_basename,
11617
+ "error": {
11618
+ "code": "terminal_injection_queued",
11619
+ "message": "Text was queued but was not typed into Terminal.",
11620
+ },
11768
11621
  }).encode()
11769
11622
  else:
11770
11623
  body = json.dumps({
@@ -11772,7 +11625,7 @@ class Handler(BaseHTTPRequestHandler):
11772
11625
  "window_match": project_basename,
11773
11626
  }).encode()
11774
11627
 
11775
- self.send_response(200)
11628
+ self.send_response(status)
11776
11629
  self.send_header("Content-Type", "application/json")
11777
11630
  self.send_header("Content-Length", str(len(body)))
11778
11631
  self.end_headers()
@@ -14152,7 +14005,7 @@ Worker instructions:
14152
14005
  self.send_error(409, "project has uncommitted changes; set allow_dirty_project to continue")
14153
14006
  return
14154
14007
 
14155
- health = _health_payload(full_power=True)
14008
+ health = _health_payload()
14156
14009
  coordinator_meta, preflight_meta = _orchestration_preflight_from_health(health)
14157
14010
  if isinstance(payload.get("coordinator"), dict):
14158
14011
  coordinator_meta.update(payload["coordinator"])
@@ -18654,7 +18507,7 @@ Worker instructions:
18654
18507
  def _pairdrop_store(self):
18655
18508
  if PairDropStore is None:
18656
18509
  raise RuntimeError("PairDrop store unavailable")
18657
- return PairDropStore(HOME / "Pairling" / "PairDrop" / "v1")
18510
+ return PairDropStore(HOME / "PairDrop")
18658
18511
 
18659
18512
  def _pairdrop_source(self) -> tuple[str, str]:
18660
18513
  auth = getattr(self, "pairling_auth", None)