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.
- package/README.md +1 -1
- package/package.json +5 -5
- package/payload/mac/SOURCE_BRANCH +1 -1
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/app_attest_lan.py +87 -0
- package/payload/mac/companiond/codex_approval.py +69 -0
- package/payload/mac/companiond/pairling_devices.py +35 -0
- package/payload/mac/companiond/pairling_pairing.py +374 -70
- package/payload/mac/companiond/pairling_psk.py +100 -0
- package/payload/mac/companiond/pairling_tools.py +2 -2
- package/payload/mac/companiond/pairlingd.py +977 -104
- package/payload/mac/companiond/pty_broker.py +441 -3
- package/payload/mac/companiond/pty_broker_client.py +167 -0
- package/payload/mac/companiond/pty_broker_service.py +84 -0
- package/payload/mac/companiond/runtime_contract.py +0 -2
- package/payload/mac/companiond/standard_push_publisher.py +7 -0
- package/payload/mac/connectd/cmd/pairling-connectd/authkey_test.go +47 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +41 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +1 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +1 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +1 -1
- package/payload/mac/connectd/internal/status/status.go +9 -0
- package/payload/mac/install/doctor.sh +227 -51
- package/payload/mac/install/install-runtime.sh +398 -15
- package/payload/mac/install/psk_dependency_check.py +40 -0
- package/payload/mac/install/render-launchd.py +23 -0
- package/payload/mac/install/uninstall-runtime.sh +4 -12
- 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
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
247
|
-
"label": "Start Pairling
|
|
248
|
-
"message": "Run
|
|
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": "
|
|
252
|
-
"label": "Install Pairling
|
|
253
|
-
"message": "
|
|
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
|
-
|
|
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/
|
|
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()}/{
|
|
429
|
-
|
|
430
|
-
add("
|
|
431
|
-
add("
|
|
432
|
-
|
|
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("
|
|
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"
|
|
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
|
-
|
|
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(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
644
|
+
{"profile": notary_profile, "authenticated": has_notary_profile, "error": None if has_notary_profile else (err or out)[:200]},
|
|
502
645
|
)
|
|
503
646
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
"
|
|
521
|
-
|
|
664
|
+
"connectd_signature",
|
|
665
|
+
signed_ok,
|
|
522
666
|
"warning",
|
|
523
|
-
"
|
|
524
|
-
{"
|
|
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("
|
|
671
|
+
release_blockers.append("Staged pairling-connectd is not present; run pairling setup.")
|
|
531
672
|
add(
|
|
532
|
-
"
|
|
673
|
+
"connectd_signature",
|
|
533
674
|
False,
|
|
534
675
|
"warning",
|
|
535
|
-
"
|
|
536
|
-
{"
|
|
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,
|