pairling 0.2.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 +160 -18
  25. package/payload/mac/install/install-runtime.sh +329 -12
  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
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import signal
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from pty_broker import PTYBrokerManager, ensure_pty_broker_token
11
+
12
+
13
+ def _app_support_root() -> Path:
14
+ raw = os.environ.get("PAIRLING_APP_SUPPORT_ROOT")
15
+ if raw:
16
+ return Path(raw).expanduser()
17
+ return Path.home() / "Library" / "Application Support" / "Pairling"
18
+
19
+
20
+ def _runtime_root() -> Path:
21
+ return Path(__file__).absolute().parent.parent
22
+
23
+
24
+ def _source_revision(runtime_root: Path) -> str | None:
25
+ for path in [runtime_root / "manifest.json", runtime_root / "mac" / "SOURCE_REVISION", runtime_root / "SOURCE_REVISION"]:
26
+ try:
27
+ if path.name == "manifest.json":
28
+ import json
29
+
30
+ payload = json.loads(path.read_text(encoding="utf-8"))
31
+ revision = payload.get("source_revision")
32
+ return str(revision) if revision else None
33
+ revision = path.read_text(encoding="utf-8").strip()
34
+ return revision or None
35
+ except FileNotFoundError:
36
+ continue
37
+ except Exception:
38
+ continue
39
+ return None
40
+
41
+
42
+ def main() -> int:
43
+ home = Path.home()
44
+ companion_dir = home / ".claude" / "companion"
45
+ terminal_capture_dir = companion_dir / "terminal-capture"
46
+ token = ensure_pty_broker_token(companion_dir)
47
+ socket_path = companion_dir / "pty-broker.sock"
48
+ runtime_root = _runtime_root()
49
+ manager = PTYBrokerManager(
50
+ socket_path=socket_path,
51
+ log_dir=terminal_capture_dir,
52
+ token=token,
53
+ runtime_root=runtime_root,
54
+ script_path=Path(__file__).absolute(),
55
+ source_revision=_source_revision(runtime_root),
56
+ )
57
+
58
+ stopping = False
59
+
60
+ def _handle_signal(signum, _frame) -> None:
61
+ nonlocal stopping
62
+ stopping = True
63
+ print(
64
+ f"pairling pty broker received {signal.Signals(signum).name}; live PTYs will hang up if the service exits",
65
+ file=sys.stderr,
66
+ flush=True,
67
+ )
68
+
69
+ signal.signal(signal.SIGTERM, _handle_signal)
70
+ signal.signal(signal.SIGINT, _handle_signal)
71
+
72
+ print(
73
+ f"pairling pty broker starting socket={socket_path} app_support={_app_support_root()}",
74
+ file=sys.stderr,
75
+ flush=True,
76
+ )
77
+ manager.start_attach_server()
78
+ while not stopping:
79
+ time.sleep(1.0)
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(main())
@@ -15,8 +15,6 @@ PORT = int(os.environ.get("PAIRLING_RUNTIME_PORT", "7773"))
15
15
  LEGACY_PORT = 7723
16
16
  DAEMON_LABEL = "dev.pairling.companiond"
17
17
  GUARDIAN_LABEL = "dev.pairling.power-guardian"
18
- LEGACY_DAEMON_LABEL = "com.mghome.notify-webhook"
19
- LEGACY_GUARDIAN_LABEL = "com.mghome.companion-power-guardian"
20
18
  LEGACY_TOKEN_RELATIVE_PATH = ".claude/scripts/.notify-token"
21
19
  POWER_STATE_PATH = "/var/run/pairling-power-state.json"
22
20
  TAILSCALE_VARIANT = "standalone"
@@ -143,6 +143,8 @@ class TurnStateAlertPublisher:
143
143
  "last_update": last_update,
144
144
  "provider": provider,
145
145
  "tool": _bounded_optional(payload.get("tool"), 80),
146
+ "request_nonce": _bounded_optional(payload.get("request_nonce"), 160),
147
+ "mac_install_id": _bounded_optional(payload.get("mac_install_id"), 160),
146
148
  }
147
149
  return {
148
150
  "state_key": path.stem,
@@ -211,6 +213,11 @@ class TurnStateAlertPublisher:
211
213
  "session_id": native_id,
212
214
  "provider": provider,
213
215
  })
216
+ if kind == "action_required":
217
+ if visible.get("request_nonce"):
218
+ payload["request_nonce"] = visible.get("request_nonce")
219
+ if visible.get("mac_install_id"):
220
+ payload["mac_install_id"] = visible.get("mac_install_id")
214
221
  return payload
215
222
 
216
223
  def _route_session_id_for_event(self, session_id: str) -> str:
@@ -0,0 +1,47 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ func TestDefaultAuthKeyTag(t *testing.T) {
10
+ t.Setenv("PAIRLING_TS_AUTHKEY_TAG", "")
11
+ if got := defaultAuthKeyTag(); got != "tag:pairling-connect" {
12
+ t.Fatalf("default tag = %q, want tag:pairling-connect", got)
13
+ }
14
+ t.Setenv("PAIRLING_TS_AUTHKEY_TAG", "tag:custom")
15
+ if got := defaultAuthKeyTag(); got != "tag:custom" {
16
+ t.Fatalf("override tag = %q, want tag:custom", got)
17
+ }
18
+ }
19
+
20
+ func TestLoadTailscaleAuthKeyPrefersEnv(t *testing.T) {
21
+ dir := t.TempDir()
22
+ connectdDir := filepath.Join(dir, "connectd")
23
+ if err := os.MkdirAll(connectdDir, 0o700); err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ if err := os.WriteFile(filepath.Join(connectdDir, "connectd-ts-authkey"), []byte("tskey-auth-from-file\n"), 0o600); err != nil {
27
+ t.Fatal(err)
28
+ }
29
+
30
+ t.Setenv("PAIRLING_TS_AUTHKEY", " tskey-auth-from-env ")
31
+ if got := loadTailscaleAuthKey(dir); got != "tskey-auth-from-env" {
32
+ t.Fatalf("env precedence failed: got %q", got)
33
+ }
34
+
35
+ t.Setenv("PAIRLING_TS_AUTHKEY", "")
36
+ if got := loadTailscaleAuthKey(dir); got != "tskey-auth-from-file" {
37
+ t.Fatalf("file fallback failed: got %q", got)
38
+ }
39
+ }
40
+
41
+ func TestLoadTailscaleAuthKeyAbsentIsInteractive(t *testing.T) {
42
+ dir := t.TempDir()
43
+ t.Setenv("PAIRLING_TS_AUTHKEY", "")
44
+ if got := loadTailscaleAuthKey(dir); got != "" {
45
+ t.Fatalf("expected empty (interactive) when no key present, got %q", got)
46
+ }
47
+ }
@@ -47,6 +47,11 @@ func run(args []string) int {
47
47
  controlURL := fs.String("control-url", "", "advanced: custom Tailscale-compatible control server URL")
48
48
  maxBodyBytes := fs.Int64("max-body-bytes", 1_000_000, "maximum proxied request body size")
49
49
  verbose := fs.Bool("verbose", false, "enable verbose tsnet backend logs")
50
+ // WS1: a tagged auth key (minted from an OAuth client scoped to
51
+ // tag:pairling-connect) registers this node pre-authorized AND with key
52
+ // expiry disabled — no 180-day re-auth cliff, no per-node REST call.
53
+ // Empty keeps the legacy interactive browser-auth path (back-compat).
54
+ authKeyTag := fs.String("auth-key-tag", defaultAuthKeyTag(), "tag applied to this node when registering with an auth key")
50
55
 
51
56
  if err := fs.Parse(args); err != nil {
52
57
  if errors.Is(err, flag.ErrHelp) {
@@ -86,6 +91,18 @@ func run(args []string) int {
86
91
  ControlURL: strings.TrimSpace(*controlURL),
87
92
  UserLogf: userLogf(statusStore),
88
93
  }
94
+ // WS1: tagged auth-key registration. A tagged node never expires its key,
95
+ // eliminating the 180-day re-auth cliff without any Tailscale REST call.
96
+ if authKey := loadTailscaleAuthKey(appSupport); authKey != "" {
97
+ srv.AuthKey = authKey
98
+ if tag := strings.TrimSpace(*authKeyTag); tag != "" {
99
+ srv.AdvertiseTags = []string{tag}
100
+ }
101
+ statusStore.SetAuthKeyMode("tagged")
102
+ log.Printf("pairling-connectd registering with tagged auth key (tag=%s)", strings.TrimSpace(*authKeyTag))
103
+ } else {
104
+ statusStore.SetAuthKeyMode("interactive")
105
+ }
89
106
  if *verbose {
90
107
  srv.Logf = func(format string, args ...any) {
91
108
  log.Printf("tsnet: "+format, args...)
@@ -146,6 +163,30 @@ func controlURLMode(raw string) string {
146
163
  return status.CustomControlURLMode
147
164
  }
148
165
 
166
+ // defaultAuthKeyTag is the ACL tag applied to Pairling Connect nodes that
167
+ // register with a tagged auth key. Tagged nodes do not expire their keys.
168
+ func defaultAuthKeyTag() string {
169
+ if tag := strings.TrimSpace(os.Getenv("PAIRLING_TS_AUTHKEY_TAG")); tag != "" {
170
+ return tag
171
+ }
172
+ return "tag:pairling-connect"
173
+ }
174
+
175
+ // loadTailscaleAuthKey resolves a tagged auth key, preferring the environment
176
+ // (PAIRLING_TS_AUTHKEY) over a mode-600 credential file under Application
177
+ // Support. Returns "" when neither is present (legacy interactive auth).
178
+ func loadTailscaleAuthKey(appSupport string) string {
179
+ if key := strings.TrimSpace(os.Getenv("PAIRLING_TS_AUTHKEY")); key != "" {
180
+ return key
181
+ }
182
+ path := filepath.Join(appSupport, "connectd", "connectd-ts-authkey")
183
+ data, err := os.ReadFile(path)
184
+ if err != nil {
185
+ return ""
186
+ }
187
+ return strings.TrimSpace(string(data))
188
+ }
189
+
149
190
  func listenPort(addr string) int {
150
191
  addr = strings.TrimSpace(addr)
151
192
  if addr == "" {
@@ -503,6 +503,7 @@ var postPaths = map[string]bool{
503
503
  "/phone-tools/result": true,
504
504
  "/push/live-activity-test": true,
505
505
  "/push/live-activity-token": true,
506
+ "/push/permission/allow": true,
506
507
  "/push/preferences": true,
507
508
  "/push/test": true,
508
509
  "/resume-session": true,
@@ -349,6 +349,7 @@ func TestPrePairModeOnlyAllowsHealthManifestAndClaim(t *testing.T) {
349
349
  {http.MethodPost, "/terminal-control", http.StatusNotFound},
350
350
  {http.MethodPost, "/worker-kill", http.StatusNotFound},
351
351
  {http.MethodPost, "/push/preferences", http.StatusNotFound},
352
+ {http.MethodPost, "/push/permission/allow", http.StatusNotFound},
352
353
  {http.MethodPost, "/safety/ack", http.StatusNotFound},
353
354
  {http.MethodPost, "/aperture-cli/open", http.StatusNotFound},
354
355
  {http.MethodGet, "/health", http.StatusOK},
@@ -6,7 +6,7 @@ import (
6
6
  )
7
7
 
8
8
  func TestDefaultStateDirUsesPairlingApplicationSupport(t *testing.T) {
9
- home := filepath.Join(string(filepath.Separator), "Users", "mergim")
9
+ home := filepath.Join(string(filepath.Separator), "Users", "example")
10
10
  got := DefaultStateDir(home)
11
11
  want := filepath.Join(home, "Library", "Application Support", "Pairling", "connectd", "tsnet-state")
12
12
  if got != want {
@@ -43,6 +43,7 @@ type Snapshot struct {
43
43
  TailnetIPCount int `json:"tailnet_ip_count"`
44
44
  AuthURLPresent bool `json:"auth_url_present"`
45
45
  ControlURLMode string `json:"control_url_mode"`
46
+ AuthKeyMode string `json:"auth_key_mode,omitempty"`
46
47
  UpstreamReachable bool `json:"upstream_reachable"`
47
48
  ListenerRunning bool `json:"listener_running"`
48
49
  GatewayHealthy bool `json:"gateway_healthy"`
@@ -88,6 +89,14 @@ func (s *Store) SetControlURLMode(mode string) {
88
89
  })
89
90
  }
90
91
 
92
+ // SetAuthKeyMode records how this node authenticated: "tagged" (a tagged
93
+ // auth key, key expiry disabled) or "interactive" (the legacy browser flow).
94
+ func (s *Store) SetAuthKeyMode(mode string) {
95
+ s.update(func(snapshot *Snapshot) {
96
+ snapshot.AuthKeyMode = mode
97
+ })
98
+ }
99
+
91
100
  func (s *Store) SetListenPort(port int) {
92
101
  s.update(func(snapshot *Snapshot) {
93
102
  if port <= 0 {
@@ -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"
@@ -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,7 +641,7 @@ 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
647
  # npm distribution: the staged pairling-connectd binary must be a valid
@@ -654,6 +797,7 @@ result = {
654
797
  "launchd_label": PAIRLING_LABEL,
655
798
  "guardian_label": PAIRLING_GUARDIAN_LABEL,
656
799
  },
800
+ "ptybroker": ptybroker_deployment,
657
801
  "paths": {
658
802
  "app_support": str(APP_SUPPORT),
659
803
  "logs": str(LOGS_ROOT),
@@ -662,9 +806,7 @@ result = {
662
806
  "pair_records": str(PAIR_ROOT),
663
807
  },
664
808
  "legacy": {
665
- "daemon_label": LEGACY_LABEL,
666
809
  "port": 7723,
667
- "loaded": legacy_loaded,
668
810
  "listeners": listeners_7723,
669
811
  },
670
812
  "release_blockers": release_blockers,