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.
- package/README.md +6 -7
- package/package.json +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairling_pairing.py +13 -7
- package/payload/mac/companiond/pairlingd.py +160 -307
- package/payload/mac/companiond/runtime_contract.py +0 -3
- package/payload/mac/companiond/runtime_manifest.py +2 -1
- package/payload/mac/companiond/runtime_paths.py +2 -6
- package/payload/mac/companiond/safety_monitor.py +56 -1
- package/payload/mac/connectd/internal/gateway/proxy.go +13 -1
- package/payload/mac/connectd/internal/gateway/proxy_test.go +24 -0
- package/payload/mac/install/bootstrap-first-run.sh +32 -1
- package/payload/mac/install/doctor.sh +43 -14
- package/payload/mac/install/install-runtime.sh +812 -50
- package/payload/mac/install/render-launchd.py +1 -28
- package/payload/mac/install/uninstall-runtime.sh +0 -3
- package/payload/mac/install/verify-payload-manifest.py +71 -0
- package/payload-manifest.json +23 -27
- package/payload/mac/guardian/companion-power-guardian.py +0 -613
- package/payload/mac/guardian/guardian_contract.py +0 -67
|
@@ -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", "/
|
|
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
|
|
940
|
-
#
|
|
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", "/
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
2533
|
-
entries = _listener_entries(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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":
|
|
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":
|
|
2720
|
-
"lid_closed":
|
|
2721
|
-
"ac_power":
|
|
2722
|
-
"low_power_mode":
|
|
2723
|
-
"thermal":
|
|
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
|
|
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
|
|
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
|
-
|
|
7936
|
+
provider,
|
|
8118
7937
|
session_id,
|
|
8119
7938
|
project,
|
|
8120
|
-
pid=self.
|
|
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.
|
|
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
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
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 + /
|
|
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":
|
|
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(
|
|
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(
|
|
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 / "
|
|
18510
|
+
return PairDropStore(HOME / "PairDrop")
|
|
18658
18511
|
|
|
18659
18512
|
def _pairdrop_source(self) -> tuple[str, str]:
|
|
18660
18513
|
auth = getattr(self, "pairling_auth", None)
|