pairling 0.1.0 → 0.2.1

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.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/package.json +5 -5
  3. package/payload/mac/SOURCE_BRANCH +1 -1
  4. package/payload/mac/SOURCE_REVISION +1 -1
  5. package/payload/mac/VERSION +1 -1
  6. package/payload/mac/companiond/app_attest_lan.py +87 -0
  7. package/payload/mac/companiond/codex_approval.py +69 -0
  8. package/payload/mac/companiond/pairling_devices.py +35 -0
  9. package/payload/mac/companiond/pairling_pairing.py +374 -70
  10. package/payload/mac/companiond/pairling_psk.py +100 -0
  11. package/payload/mac/companiond/pairling_tools.py +2 -2
  12. package/payload/mac/companiond/pairlingd.py +977 -104
  13. package/payload/mac/companiond/pty_broker.py +441 -3
  14. package/payload/mac/companiond/pty_broker_client.py +167 -0
  15. package/payload/mac/companiond/pty_broker_service.py +84 -0
  16. package/payload/mac/companiond/runtime_contract.py +0 -2
  17. package/payload/mac/companiond/standard_push_publisher.py +7 -0
  18. package/payload/mac/connectd/cmd/pairling-connectd/authkey_test.go +47 -0
  19. package/payload/mac/connectd/cmd/pairling-connectd/main.go +41 -0
  20. package/payload/mac/connectd/internal/gateway/proxy.go +1 -0
  21. package/payload/mac/connectd/internal/gateway/proxy_test.go +1 -0
  22. package/payload/mac/connectd/internal/runtime/config_test.go +1 -1
  23. package/payload/mac/connectd/internal/status/status.go +9 -0
  24. package/payload/mac/install/doctor.sh +227 -51
  25. package/payload/mac/install/install-runtime.sh +398 -15
  26. package/payload/mac/install/psk_dependency_check.py +40 -0
  27. package/payload/mac/install/render-launchd.py +23 -0
  28. package/payload/mac/install/uninstall-runtime.sh +4 -12
  29. package/payload-manifest.json +51 -23
@@ -62,7 +62,8 @@ PAIRLING_PORT = int(os.environ.get("PAIRLING_RUNTIME_PORT", "7773"))
62
62
  PAIRLING_LABEL = "dev.pairling.companiond"
63
63
  PAIRLING_GUARDIAN_LABEL = "dev.pairling.power-guardian"
64
64
  PAIRLING_CONNECTD_LABEL = "dev.pairling.connectd"
65
- LEGACY_LABEL = "com.mghome.notify-webhook"
65
+ PAIRLING_PTYBROKER_LABEL = "dev.pairling.ptybroker"
66
+ TEAM_ID = os.environ.get("PAIRLING_TEAM_ID", os.environ.get("PAIRLING_CONNECTD_TEAM_ID", "965AVD34A3"))
66
67
  APP_SUPPORT = Path(os.environ.get("PAIRLING_APP_SUPPORT_ROOT", os.environ.get("COMPANION_APP_SUPPORT_ROOT", str(home / "Library" / "Application Support" / "Pairling"))))
67
68
  LOGS_ROOT = Path(os.environ.get("PAIRLING_LOGS_ROOT", os.environ.get("COMPANION_LOGS_ROOT", str(home / "Library" / "Logs" / "Pairling"))))
68
69
  CURRENT = APP_SUPPORT / "runtime" / "current"
@@ -75,9 +76,8 @@ USER_PAIRLING = home / ".local" / "bin" / "pairling"
75
76
  PAIR_ROOT = APP_SUPPORT / "pair"
76
77
  USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_LABEL}.plist"
77
78
  CONNECTD_USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_CONNECTD_LABEL}.plist"
78
- LEGACY_USER_PLIST = home / "Library" / "LaunchAgents" / f"{LEGACY_LABEL}.plist"
79
+ PTYBROKER_USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_PTYBROKER_LABEL}.plist"
79
80
  SYSTEM_PLIST = Path("/Library/LaunchDaemons") / f"{PAIRLING_GUARDIAN_LABEL}.plist"
80
- LEGACY_SYSTEM_PLIST = Path("/Library/LaunchDaemons/com.mghome.companion-power-guardian.plist")
81
81
 
82
82
  sys.path.insert(0, str(repo_root / "mac" / "companiond"))
83
83
  from pairling_connectd_status import fetch_connectd_status, redacted_connectd_summary
@@ -95,6 +95,23 @@ def add(identifier, ok, severity, summary, evidence=None):
95
95
  })
96
96
 
97
97
 
98
+ def codesigning_identity_summary(output: str) -> dict:
99
+ lines = [line.strip() for line in output.splitlines() if line.strip()]
100
+ developer_id_lines = [line for line in lines if "Developer ID Application:" in line]
101
+ expected_team_present = any(f"({TEAM_ID})" in line for line in developer_id_lines)
102
+ valid_count = None
103
+ for line in lines:
104
+ match = re.search(r"(\d+)\s+valid identities found", line)
105
+ if match:
106
+ valid_count = int(match.group(1))
107
+ break
108
+ return {
109
+ "valid_identity_count": valid_count,
110
+ "developer_id_application_count": len(developer_id_lines),
111
+ "expected_team_present": expected_team_present,
112
+ }
113
+
114
+
98
115
  def run(args, timeout=5):
99
116
  try:
100
117
  proc = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
@@ -202,6 +219,103 @@ def active_pair_records(pair_root: Path) -> list[dict]:
202
219
  return records
203
220
 
204
221
 
222
+ def desired_ptybroker_identity() -> dict:
223
+ revision = None
224
+ if manifest and manifest.get("source_revision"):
225
+ revision = str(manifest.get("source_revision"))
226
+ elif (CURRENT / "mac" / "SOURCE_REVISION").is_file():
227
+ try:
228
+ revision = (CURRENT / "mac" / "SOURCE_REVISION").read_text().strip() or None
229
+ except Exception:
230
+ revision = None
231
+ desired_root = CURRENT.resolve() if CURRENT.exists() else CURRENT
232
+ return {
233
+ "runtime_root": str(desired_root),
234
+ "script_path": str(desired_root / "companiond" / "pty_broker_service.py"),
235
+ "source_revision": revision,
236
+ "protocol_version": 1,
237
+ }
238
+
239
+
240
+ def ptybroker_status_rpc() -> tuple[dict | None, str | None]:
241
+ try:
242
+ sys.path.insert(0, str(CURRENT / "companiond"))
243
+ from pty_broker_client import PTYBrokerClient, ensure_pty_broker_token
244
+
245
+ companion = home / ".claude" / "companion"
246
+ client = PTYBrokerClient(companion / "pty-broker.sock", ensure_pty_broker_token(companion), timeout=1.0)
247
+ status = client.status()
248
+ return status if isinstance(status, dict) else {}, None
249
+ except Exception as exc:
250
+ return None, f"{type(exc).__name__}: {exc}"
251
+
252
+
253
+ def ptybroker_deployment_status(*, launchd_loaded: bool) -> dict:
254
+ desired = desired_ptybroker_identity()
255
+ base = {
256
+ "label": PAIRLING_PTYBROKER_LABEL,
257
+ "state": "unknown",
258
+ "restart_deferred": False,
259
+ "pid": None,
260
+ "live_session_count": None,
261
+ "live_source_revision": None,
262
+ "desired_source_revision": desired.get("source_revision"),
263
+ "desired_runtime_root": desired.get("runtime_root"),
264
+ "desired_script_path": desired.get("script_path"),
265
+ "evidence": None,
266
+ }
267
+ if not MANIFEST_PATH.is_file() and not CURRENT.exists():
268
+ return {**base, "state": "not_installed", "evidence": "runtime/current is missing"}
269
+ if not launchd_loaded:
270
+ return {**base, "state": "not_running", "evidence": "launchd label is not running"}
271
+ live, error = ptybroker_status_rpc()
272
+ if live is None:
273
+ return {**base, "state": "unreachable_socket", "evidence": error}
274
+ reasons = []
275
+ live_root = live.get("runtime_root")
276
+ if live_root:
277
+ if os.path.realpath(str(live_root)) != str(desired.get("runtime_root")):
278
+ reasons.append("runtime_root_mismatch")
279
+ else:
280
+ reasons.append("runtime_root_missing")
281
+ live_script = live.get("script_path")
282
+ if live_script:
283
+ if os.path.realpath(str(live_script)) != str(desired.get("script_path")):
284
+ reasons.append("script_path_mismatch")
285
+ else:
286
+ reasons.append("script_path_missing")
287
+ live_revision = live.get("source_revision")
288
+ if desired.get("source_revision") and not live_revision:
289
+ reasons.append("source_revision_missing")
290
+ elif live_revision and desired.get("source_revision") and str(live_revision) != str(desired.get("source_revision")):
291
+ reasons.append("source_revision_mismatch")
292
+ try:
293
+ live_protocol = int(live.get("protocol_version") or 0)
294
+ except (TypeError, ValueError):
295
+ live_protocol = 0
296
+ if live_protocol != int(desired.get("protocol_version") or 0):
297
+ if not live.get("protocol_version"):
298
+ reasons.append("protocol_version_missing")
299
+ else:
300
+ reasons.append("protocol_version_mismatch")
301
+ state = "current" if not reasons else "stale_deferred"
302
+ return {
303
+ **base,
304
+ "state": state,
305
+ "restart_deferred": state == "stale_deferred",
306
+ "pid": live.get("pid"),
307
+ "live_session_count": live.get("live_session_count"),
308
+ "live_source_revision": live_revision,
309
+ "live_runtime_root": live_root,
310
+ "live_script_path": live_script,
311
+ "protocol_version": live.get("protocol_version"),
312
+ "code_version": live.get("code_version"),
313
+ "started_at": live.get("started_at"),
314
+ "reasons": reasons,
315
+ "evidence": live,
316
+ }
317
+
318
+
205
319
  def first_run_stage(*, installed: bool, running: bool, pair_window_open: bool, remote_ready: bool) -> str:
206
320
  if not installed:
207
321
  return "helper_missing"
@@ -239,18 +353,18 @@ def next_action_for_stage(stage: str, *, remote_status: str, pair_window_open: b
239
353
  return {
240
354
  "id": "open_pairing_invitation",
241
355
  "label": "Open pairing invitation",
242
- "message": "Open a pairing invitation from Pairling Helper, then pair from the iPhone.",
356
+ "message": "Run pairling pair to open a pairing invitation, then pair from the iPhone.",
243
357
  }
244
358
  if stage == "runtime_not_ready":
245
359
  return {
246
- "id": "start_helper",
247
- "label": "Start Pairling Helper",
248
- "message": "Run Pairling Helper setup and review the failing runtime checks.",
360
+ "id": "start_runtime",
361
+ "label": "Start the Pairling runtime",
362
+ "message": "Run pairling setup and review the failing runtime checks.",
249
363
  }
250
364
  return {
251
- "id": "install_helper",
252
- "label": "Install Pairling Helper",
253
- "message": "Install Pairling Helper on this Mac before pairing.",
365
+ "id": "install_cli",
366
+ "label": "Install the Pairling CLI",
367
+ "message": "Run npm install -g pairling then pairling setup on this Mac before pairing.",
254
368
  }
255
369
 
256
370
 
@@ -269,7 +383,13 @@ if manifest:
269
383
  runtime = manifest.get("runtime") if isinstance(manifest.get("runtime"), dict) else {}
270
384
  add("runtime_port", runtime.get("port") == PAIRLING_PORT, "error", "Runtime port is locked to 7773.", runtime.get("port"))
271
385
  launchd = manifest.get("launchd") if isinstance(manifest.get("launchd"), dict) else {}
272
- add("launchd_labels", launchd.get("daemon_label") == PAIRLING_LABEL and launchd.get("connectd_label") == PAIRLING_CONNECTD_LABEL and launchd.get("guardian_label") == PAIRLING_GUARDIAN_LABEL, "error", "Manifest launchd labels are Pairling labels.", launchd)
386
+ launchd_evidence = {
387
+ "daemon_label": launchd.get("daemon_label"),
388
+ "ptybroker_label": launchd.get("ptybroker_label"),
389
+ "connectd_label": launchd.get("connectd_label"),
390
+ "guardian_label": launchd.get("guardian_label"),
391
+ }
392
+ add("launchd_labels", launchd.get("daemon_label") == PAIRLING_LABEL and launchd.get("ptybroker_label") == PAIRLING_PTYBROKER_LABEL and launchd.get("connectd_label") == PAIRLING_CONNECTD_LABEL and launchd.get("guardian_label") == PAIRLING_GUARDIAN_LABEL, "error", "Manifest launchd labels are Pairling labels.", launchd_evidence)
273
393
  mismatches = []
274
394
  for item in manifest.get("files") or []:
275
395
  rel = item.get("path")
@@ -365,7 +485,7 @@ try:
365
485
  add(
366
486
  "shell_pairling_wrapper",
367
487
  "runtime/current/bin/pairling" in pairling_text
368
- and "/Users/mergimg0/projects/Pairling" not in pairling_text,
488
+ and re.search(r"/Users/[^\\s'\"]+/projects/Pairling", pairling_text) is None,
369
489
  "error",
370
490
  "User pairling command resolves through runtime/current unless PAIRLING_REPO_ROOT is explicitly set.",
371
491
  str(USER_PAIRLING),
@@ -411,6 +531,19 @@ except Exception as exc:
411
531
  add("connectd_launchagent_plist", False, "error", f"Pairling Connect LaunchAgent plist unreadable: {type(exc).__name__}: {exc}", str(CONNECTD_USER_PLIST))
412
532
  add("connectd_launchagent_env", False, "error", "Cannot validate Pairling Connect LaunchAgent environment.", str(CONNECTD_USER_PLIST))
413
533
 
534
+ try:
535
+ payload = load_plist(PTYBROKER_USER_PLIST)
536
+ args = payload.get("ProgramArguments") or []
537
+ add(
538
+ "ptybroker_launchagent_plist",
539
+ payload.get("Label") == PAIRLING_PTYBROKER_LABEL and any(str(CURRENT / "companiond" / "pty_broker_service.py") == value for value in args),
540
+ "error",
541
+ "Pairling PTY broker LaunchAgent points at runtime/current.",
542
+ {"label": payload.get("Label"), "args": args},
543
+ )
544
+ except Exception as exc:
545
+ add("ptybroker_launchagent_plist", False, "error", f"Pairling PTY broker LaunchAgent plist unreadable: {type(exc).__name__}: {exc}", str(PTYBROKER_USER_PLIST))
546
+
414
547
  try:
415
548
  payload = load_plist(SYSTEM_PLIST)
416
549
  add("guardian_plist", payload.get("Label") == PAIRLING_GUARDIAN_LABEL, "warning", "Pairling guardian LaunchDaemon is rendered/installed.", {"label": payload.get("Label")})
@@ -425,16 +558,23 @@ code, out, err = run(["launchctl", "print", f"gui/{os.getuid()}/{PAIRLING_CONNEC
425
558
  add("connectd_launchagent_loaded", code == 0 and "state = running" in out, "error", "Pairling Connect LaunchAgent is running." if code == 0 else "Pairling Connect LaunchAgent is not loaded.", (out or err)[:2000])
426
559
  add("connectd_loaded_from_current", str(CURRENT / "connectd" / "pairling-connectd") in out, "error", "Loaded Pairling Connect LaunchAgent uses runtime/current.", out[:2000])
427
560
 
428
- code, out, err = run(["launchctl", "print", f"gui/{os.getuid()}/{LEGACY_LABEL}"])
429
- legacy_loaded = code == 0 and "state = running" in out
430
- add("legacy_daemon_unloaded", not legacy_loaded, "error", "Old Pairling predecessor launchd label is not loaded.", (out or err)[:2000])
431
- add("legacy_launchagent_removed", not LEGACY_USER_PLIST.exists(), "warning", "Legacy user LaunchAgent plist is absent.", str(LEGACY_USER_PLIST))
432
- add("legacy_guardian_removed", not LEGACY_SYSTEM_PLIST.exists(), "warning", "Legacy guardian LaunchDaemon plist is absent.", str(LEGACY_SYSTEM_PLIST))
561
+ code, out, err = run(["launchctl", "print", f"gui/{os.getuid()}/{PAIRLING_PTYBROKER_LABEL}"])
562
+ ptybroker_launchd_loaded = code == 0 and "state = running" in out
563
+ add("ptybroker_launchagent_loaded", ptybroker_launchd_loaded, "error", "Pairling PTY broker LaunchAgent is running." if code == 0 else "Pairling PTY broker LaunchAgent is not loaded.", (out or err)[:2000])
564
+ add("ptybroker_loaded_from_current", str(CURRENT / "companiond" / "pty_broker_service.py") in out, "error", "Loaded Pairling PTY broker uses runtime/current.", out[:2000])
565
+ ptybroker_deployment = ptybroker_deployment_status(launchd_loaded=ptybroker_launchd_loaded)
566
+ add(
567
+ "ptybroker_deployment_state",
568
+ ptybroker_deployment["state"] == "current",
569
+ "warning",
570
+ f"Pairling PTY broker deployment state is {ptybroker_deployment['state']}.",
571
+ ptybroker_deployment,
572
+ )
433
573
 
434
574
  listeners_7773 = port_listeners(PAIRLING_PORT)
435
575
  listeners_7723 = port_listeners(7723)
436
576
  add("port_7773_listener", bool(listeners_7773) or tcp_accepts("127.0.0.1", PAIRLING_PORT), "error", "Runtime is listening on 7773.", listeners_7773)
437
- legacy_conflict = any("notify-webhook" in line or "Python" in line or "python" in line for line in listeners_7723)
577
+ legacy_conflict = any("Python" in line or "python" in line for line in listeners_7723)
438
578
  add("legacy_port_7723_clear", not legacy_conflict, "error", "Legacy 7723 daemon is not conflicting.", listeners_7723)
439
579
 
440
580
  health = None
@@ -475,17 +615,20 @@ for name in ["claude", "codex"]:
475
615
  add("provider_clis_detected", True, "warning", "Provider CLI detection completed.", provider_evidence)
476
616
 
477
617
  release_blockers = []
478
- developer_id_identity = os.environ.get("PAIRLING_DEVELOPER_ID_IDENTITY", "Developer ID Application: Mergim Gashi (965AVD34A3)")
618
+ developer_id_identity = os.environ.get("PAIRLING_DEVELOPER_ID_IDENTITY")
479
619
  code, out, err = run(["/usr/bin/security", "find-identity", "-v", "-p", "codesigning"], timeout=5)
480
- has_developer_id = code == 0 and developer_id_identity in out
620
+ identity_evidence = codesigning_identity_summary(out)
621
+ has_developer_id = code == 0 and (
622
+ (developer_id_identity in out) if developer_id_identity else identity_evidence["expected_team_present"]
623
+ )
481
624
  if not has_developer_id:
482
- release_blockers.append(f"Developer ID identity is missing from the login keychain: {developer_id_identity}")
625
+ release_blockers.append("Developer ID Application identity is missing from the login keychain for the expected team.")
483
626
  add(
484
627
  "developer_id_identity",
485
628
  has_developer_id,
486
629
  "warning",
487
630
  "Developer ID Application identity is available for public helper signing.",
488
- (out or err)[:2000],
631
+ identity_evidence if code == 0 else {"error": (err or "security find-identity failed")[:200]},
489
632
  )
490
633
 
491
634
  notary_profile = os.environ.get("PAIRLING_NOTARY_PROFILE", "pairling-notary")
@@ -498,42 +641,76 @@ add(
498
641
  has_notary_profile,
499
642
  "warning",
500
643
  "Notary credentials are stored and can authenticate.",
501
- (out or err)[:2000],
644
+ {"profile": notary_profile, "authenticated": has_notary_profile, "error": None if has_notary_profile else (err or out)[:200]},
502
645
  )
503
646
 
504
- helper_artifact = Path(os.environ.get("PAIRLING_HELPER_ARTIFACT", str(repo_root / "dist" / "PairlingHelper.dmg")))
505
- helper_bundle = APP_SUPPORT / "Pairling Helper.app"
506
- if helper_artifact.exists():
507
- code, out, err = run([
508
- "/usr/sbin/spctl",
509
- "-a",
510
- "-vv",
511
- "--type",
512
- "open",
513
- "--context",
514
- "context:primary-signature",
515
- str(helper_artifact),
516
- ], timeout=8)
517
- if code != 0:
518
- release_blockers.append("Developer ID signed/notarized helper DMG is not Gatekeeper-accepted yet.")
647
+ # npm distribution: the staged pairling-connectd binary must be a valid
648
+ # Developer ID build from the pinned team. This replaces the retired dmg
649
+ # Gatekeeper check; the npm install path never sets com.apple.quarantine, so
650
+ # Gatekeeper assessment is not in the launch path, but signature + Team ID
651
+ # verification is the integrity equivalent and matches the fail-closed staging
652
+ # gate in install-runtime.sh.
653
+ expected_team = os.environ.get("PAIRLING_CONNECTD_TEAM_ID", "965AVD34A3")
654
+ staged_connectd = CURRENT / "connectd" / "pairling-connectd"
655
+ if staged_connectd.exists():
656
+ vcode, vout, verr = run(["/usr/bin/codesign", "--verify", "--strict", str(staged_connectd)], timeout=8)
657
+ icode, iout, ierr = run(["/usr/bin/codesign", "-dvv", str(staged_connectd)], timeout=8)
658
+ team_line = next((l for l in ((iout or "") + (ierr or "")).splitlines() if l.startswith("TeamIdentifier=")), "")
659
+ team_id = team_line.split("=", 1)[1] if "=" in team_line else ""
660
+ signed_ok = vcode == 0 and (expected_team == "-" or team_id == expected_team)
661
+ if not signed_ok:
662
+ release_blockers.append("Staged pairling-connectd is not a valid Developer ID build from the expected team.")
519
663
  add(
520
- "helper_signing_notarization",
521
- code == 0,
664
+ "connectd_signature",
665
+ signed_ok,
522
666
  "warning",
523
- "Helper DMG Gatekeeper assessment.",
524
- {"artifact": str(helper_artifact), "assessment": (out or err)[:2000]},
667
+ "Staged pairling-connectd passes codesign --verify --strict with the expected Team ID.",
668
+ {"binary": str(staged_connectd), "team_id": team_id or None, "expected_team": expected_team, "verify": (vout or verr)[:1000]},
525
669
  )
526
- elif helper_bundle.exists():
527
- code, out, err = run(["/usr/sbin/spctl", "-a", "-vv", str(helper_bundle)], timeout=8)
528
- add("helper_signing_notarization", code == 0, "warning", "Helper bundle Gatekeeper assessment.", (out or err)[:2000])
529
670
  else:
530
- release_blockers.append("Developer ID signed/notarized helper artifact is not present yet.")
671
+ release_blockers.append("Staged pairling-connectd is not present; run pairling setup.")
531
672
  add(
532
- "helper_signing_notarization",
673
+ "connectd_signature",
533
674
  False,
534
675
  "warning",
535
- "Helper artifact not present; signing/notarization remains a release blocker.",
536
- {"artifact": str(helper_artifact), "bundle": str(helper_bundle)},
676
+ "Staged pairling-connectd not present; signature verification unavailable until pairling setup runs.",
677
+ {"binary": str(staged_connectd)},
678
+ )
679
+
680
+ # P3 Python custody: when a vendored interpreter is staged, it must be a valid
681
+ # Developer ID build from the expected team with the dev.pairling.python
682
+ # identity — that scoping is the whole point (TCC grants attach to Pairling, not
683
+ # a generic python3). When no vendored python is staged (the daemon runs under a
684
+ # system python3), this check is informational, not a blocker.
685
+ expected_python_identifier = os.environ.get("PAIRLING_PYTHON_IDENTIFIER", "dev.pairling.python")
686
+ staged_python = CURRENT / "python" / "bin" / "python3"
687
+ if staged_python.exists():
688
+ pvcode, pvout, pverr = run(["/usr/bin/codesign", "--verify", "--strict", str(staged_python)], timeout=10)
689
+ picode, piout, pierr = run(["/usr/bin/codesign", "-dvv", str(staged_python)], timeout=10)
690
+ pinfo = (piout or "") + (pierr or "")
691
+ p_team = next((l.split("=", 1)[1] for l in pinfo.splitlines() if l.startswith("TeamIdentifier=")), "")
692
+ p_id = next((l.split("=", 1)[1] for l in pinfo.splitlines() if l.startswith("Identifier=")), "")
693
+ python_signed_ok = (
694
+ pvcode == 0
695
+ and (expected_team == "-" or p_team == expected_team)
696
+ and p_id == expected_python_identifier
697
+ )
698
+ if not python_signed_ok:
699
+ release_blockers.append("Staged vendored python is not a valid dev.pairling.python Developer ID build.")
700
+ add(
701
+ "python_runtime",
702
+ python_signed_ok,
703
+ "warning",
704
+ "Staged vendored CPython is signed dev.pairling.python by the expected Team ID.",
705
+ {"python": str(staged_python), "team_id": p_team or None, "identifier": p_id or None, "expected_identifier": expected_python_identifier},
706
+ )
707
+ else:
708
+ add(
709
+ "python_runtime",
710
+ True,
711
+ "warning",
712
+ "No vendored CPython staged; daemon runs under a system python3 (acceptable pre-P3-rollout).",
713
+ {"python": str(staged_python), "vendored": False},
537
714
  )
538
715
 
539
716
  errors = [c for c in checks if c["status"] != "ok" and c["severity"] == "error"]
@@ -620,6 +797,7 @@ result = {
620
797
  "launchd_label": PAIRLING_LABEL,
621
798
  "guardian_label": PAIRLING_GUARDIAN_LABEL,
622
799
  },
800
+ "ptybroker": ptybroker_deployment,
623
801
  "paths": {
624
802
  "app_support": str(APP_SUPPORT),
625
803
  "logs": str(LOGS_ROOT),
@@ -628,9 +806,7 @@ result = {
628
806
  "pair_records": str(PAIR_ROOT),
629
807
  },
630
808
  "legacy": {
631
- "daemon_label": LEGACY_LABEL,
632
809
  "port": 7723,
633
- "loaded": legacy_loaded,
634
810
  "listeners": listeners_7723,
635
811
  },
636
812
  "release_blockers": release_blockers,