pairling 0.2.0 → 0.2.2
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 +160 -18
- package/payload/mac/install/install-runtime.sh +329 -12
- 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
|
@@ -50,7 +50,7 @@ PAIRLING_RUNTIME_PORT="${PAIRLING_RUNTIME_PORT:-7773}"
|
|
|
50
50
|
PAIRLING_DAEMON_LABEL="dev.pairling.companiond"
|
|
51
51
|
PAIRLING_GUARDIAN_LABEL="dev.pairling.power-guardian"
|
|
52
52
|
PAIRLING_CONNECTD_LABEL="dev.pairling.connectd"
|
|
53
|
-
|
|
53
|
+
PAIRLING_PTYBROKER_LABEL="dev.pairling.ptybroker"
|
|
54
54
|
APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
|
|
55
55
|
RUNTIME_ROOT="$APP_SUPPORT/runtime"
|
|
56
56
|
RELEASES_ROOT="$RUNTIME_ROOT/releases"
|
|
@@ -68,9 +68,8 @@ MCP_CREDENTIAL="$APP_SUPPORT/mcp-bridge.json"
|
|
|
68
68
|
INSTALL_HISTORY="$STATE_ROOT/install-history.jsonl"
|
|
69
69
|
USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
|
|
70
70
|
CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
|
|
71
|
-
|
|
71
|
+
PTYBROKER_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_PTYBROKER_LABEL.plist"
|
|
72
72
|
SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
73
|
-
LEGACY_SYSTEM_PLIST="/Library/LaunchDaemons/com.mghome.companion-power-guardian.plist"
|
|
74
73
|
MCP_SERVER_DIR="$HOME/.claude/mcp-servers"
|
|
75
74
|
MCP_SERVER_SHIM="$MCP_SERVER_DIR/phone-tools.py"
|
|
76
75
|
PYTHON3_BIN="${PAIRLING_DAEMON_PYTHON:-${COMPANION_DAEMON_PYTHON:-$(command -v python3)}}"
|
|
@@ -88,6 +87,15 @@ log() {
|
|
|
88
87
|
printf '%s\n' "$*"
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
display_path() {
|
|
91
|
+
local path="$1"
|
|
92
|
+
case "$path" in
|
|
93
|
+
"$HOME"/*) printf '~/%s\n' "${path#"$HOME"/}" ;;
|
|
94
|
+
"$HOME") printf '~\n' ;;
|
|
95
|
+
*) printf '%s\n' "$path" ;;
|
|
96
|
+
esac
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
is_dry_run() {
|
|
92
100
|
[[ "$DRY_RUN" == "1" || "$DRY_RUN" == "true" || "$DRY_RUN" == "TRUE" ]]
|
|
93
101
|
}
|
|
@@ -128,9 +136,13 @@ run_compile_checks() {
|
|
|
128
136
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/llm_route.py"
|
|
129
137
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_tools.py"
|
|
130
138
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_pairing.py"
|
|
139
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_psk.py"
|
|
131
140
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pairling_relay_claims.py"
|
|
132
141
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/request_proof.py"
|
|
142
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/codex_approval.py"
|
|
133
143
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pty_broker.py"
|
|
144
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pty_broker_client.py"
|
|
145
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/pty_broker_service.py"
|
|
134
146
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/terminal_screen_backend.py"
|
|
135
147
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/terminal_text_sanitizer.py"
|
|
136
148
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/companiond/push_dispatcher.py"
|
|
@@ -156,9 +168,30 @@ run_compile_checks() {
|
|
|
156
168
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/companion-power-guardian.py"
|
|
157
169
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/guardian/guardian_contract.py"
|
|
158
170
|
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/install/render-launchd.py"
|
|
171
|
+
PYTHONPYCACHEPREFIX="$pycache_root" python3 -m py_compile "$REPO_ROOT/mac/install/psk_dependency_check.py"
|
|
159
172
|
rm -rf "$pycache_root"
|
|
160
173
|
}
|
|
161
174
|
|
|
175
|
+
run_psk_dependency_import_check() {
|
|
176
|
+
local python_bin="$1"
|
|
177
|
+
local companiond_path="$2"
|
|
178
|
+
local label="$3"
|
|
179
|
+
"$python_bin" "$REPO_ROOT/mac/install/psk_dependency_check.py" "$companiond_path" --label "$label"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
run_psk_dependency_checks() {
|
|
183
|
+
run_psk_dependency_import_check "$PYTHON3_BIN" "$REPO_ROOT/mac/companiond" "source-tree preflight"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
run_staged_psk_dependency_checks() {
|
|
187
|
+
local tmp="$1"
|
|
188
|
+
local staged_python="$PYTHON3_BIN"
|
|
189
|
+
if [[ -x "$tmp/python/bin/python3" ]]; then
|
|
190
|
+
staged_python="$tmp/python/bin/python3"
|
|
191
|
+
fi
|
|
192
|
+
run_psk_dependency_import_check "$staged_python" "$tmp/companiond" "staged runtime copy"
|
|
193
|
+
}
|
|
194
|
+
|
|
162
195
|
ensure_state() {
|
|
163
196
|
mkdir -p "$RELEASES_ROOT" "$STATE_ROOT" "$PAIR_ROOT" "$LOGS_ROOT" "$PLIST_BUILD_DIR" "$APP_SUPPORT/modules"
|
|
164
197
|
chmod 700 "$APP_SUPPORT" "$PAIR_ROOT" 2>/dev/null || true
|
|
@@ -248,9 +281,13 @@ copy_release() {
|
|
|
248
281
|
cp "$REPO_ROOT/mac/companiond/llm_route.py" "$tmp/companiond/"
|
|
249
282
|
cp "$REPO_ROOT/mac/companiond/pairling_tools.py" "$tmp/companiond/"
|
|
250
283
|
cp "$REPO_ROOT/mac/companiond/pairling_pairing.py" "$tmp/companiond/"
|
|
284
|
+
cp "$REPO_ROOT/mac/companiond/pairling_psk.py" "$tmp/companiond/"
|
|
251
285
|
cp "$REPO_ROOT/mac/companiond/pairling_relay_claims.py" "$tmp/companiond/"
|
|
252
286
|
cp "$REPO_ROOT/mac/companiond/request_proof.py" "$tmp/companiond/"
|
|
287
|
+
cp "$REPO_ROOT/mac/companiond/codex_approval.py" "$tmp/companiond/"
|
|
253
288
|
cp "$REPO_ROOT/mac/companiond/pty_broker.py" "$tmp/companiond/"
|
|
289
|
+
cp "$REPO_ROOT/mac/companiond/pty_broker_client.py" "$tmp/companiond/"
|
|
290
|
+
cp "$REPO_ROOT/mac/companiond/pty_broker_service.py" "$tmp/companiond/"
|
|
254
291
|
cp "$REPO_ROOT/mac/companiond/terminal_screen_backend.py" "$tmp/companiond/"
|
|
255
292
|
cp "$REPO_ROOT/mac/companiond/terminal_text_sanitizer.py" "$tmp/companiond/"
|
|
256
293
|
cp "$REPO_ROOT/mac/companiond/push_dispatcher.py" "$tmp/companiond/"
|
|
@@ -270,6 +307,7 @@ copy_release() {
|
|
|
270
307
|
cp "$REPO_ROOT/mac/guardian/guardian_contract.py" "$tmp/guardian/"
|
|
271
308
|
build_connectd_binary "$tmp/connectd/pairling-connectd"
|
|
272
309
|
stage_vendored_python "$tmp/python"
|
|
310
|
+
run_staged_psk_dependency_checks "$tmp"
|
|
273
311
|
copy_runtime_source_tree "$tmp/mac" "$tmp/connectd/pairling-connectd"
|
|
274
312
|
write_installed_pairling_launcher "$tmp/bin/pairling"
|
|
275
313
|
chmod 755 "$tmp/bin/pairling" "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py" "$tmp/guardian/companion-power-guardian.py"
|
|
@@ -301,6 +339,10 @@ copy_runtime_source_tree() {
|
|
|
301
339
|
printf '%s\n' "$BRANCH" > "$mac_root/SOURCE_BRANCH"
|
|
302
340
|
printf '%s\n' "$SOURCE_DIRTY" > "$mac_root/SOURCE_DIRTY"
|
|
303
341
|
cp "$REPO_ROOT/mac/companiond/"*.py "$mac_root/companiond/"
|
|
342
|
+
# WS2: co-locate the canonical App Attest validator with the daemon so
|
|
343
|
+
# app_attest_lan can import it in the staged runtime (the repo keeps the one
|
|
344
|
+
# source of truth in relay/). Non-fatal if absent — the gate fails closed.
|
|
345
|
+
cp "$REPO_ROOT/relay/app_attest_validator.py" "$mac_root/companiond/" 2>/dev/null || true
|
|
304
346
|
cp "$REPO_ROOT/mac/companiond/providers/"*.py "$mac_root/companiond/providers/"
|
|
305
347
|
cp "$REPO_ROOT/mac/companiond/integrations/__init__.py" "$mac_root/companiond/integrations/"
|
|
306
348
|
cp "$REPO_ROOT/mac/companiond/integrations/aperture_cli/"*.py "$mac_root/companiond/integrations/aperture_cli/"
|
|
@@ -464,9 +506,13 @@ for rel in [
|
|
|
464
506
|
"companiond/llm_route.py",
|
|
465
507
|
"companiond/pairling_tools.py",
|
|
466
508
|
"companiond/pairling_pairing.py",
|
|
509
|
+
"companiond/pairling_psk.py",
|
|
467
510
|
"companiond/pairling_relay_claims.py",
|
|
468
511
|
"companiond/request_proof.py",
|
|
512
|
+
"companiond/codex_approval.py",
|
|
469
513
|
"companiond/pty_broker.py",
|
|
514
|
+
"companiond/pty_broker_client.py",
|
|
515
|
+
"companiond/pty_broker_service.py",
|
|
470
516
|
"companiond/terminal_screen_backend.py",
|
|
471
517
|
"companiond/terminal_text_sanitizer.py",
|
|
472
518
|
"companiond/push_dispatcher.py",
|
|
@@ -519,9 +565,9 @@ manifest = {
|
|
|
519
565
|
},
|
|
520
566
|
"launchd": {
|
|
521
567
|
"daemon_label": "dev.pairling.companiond",
|
|
568
|
+
"ptybroker_label": "dev.pairling.ptybroker",
|
|
522
569
|
"connectd_label": "dev.pairling.connectd",
|
|
523
570
|
"guardian_label": "dev.pairling.power-guardian",
|
|
524
|
-
"legacy_daemon_label": "com.mghome.notify-webhook",
|
|
525
571
|
},
|
|
526
572
|
"paths": {
|
|
527
573
|
"app_support": app_support,
|
|
@@ -635,11 +681,10 @@ render_plists() {
|
|
|
635
681
|
|
|
636
682
|
unload_legacy_daemon() {
|
|
637
683
|
if is_dry_run; then
|
|
638
|
-
log "dry-run: would
|
|
684
|
+
log "dry-run: would check legacy predecessor cleanup"
|
|
639
685
|
return
|
|
640
686
|
fi
|
|
641
|
-
|
|
642
|
-
launchctl bootout "gui/$(id -u)" "$LEGACY_USER_PLIST" >/dev/null 2>&1 || true
|
|
687
|
+
return 0
|
|
643
688
|
}
|
|
644
689
|
|
|
645
690
|
start_user_agent() {
|
|
@@ -668,6 +713,259 @@ start_connectd_agent() {
|
|
|
668
713
|
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_CONNECTD_LABEL"
|
|
669
714
|
}
|
|
670
715
|
|
|
716
|
+
ptybroker_live_session_count() {
|
|
717
|
+
local status_json
|
|
718
|
+
if status_json="$(ptybroker_status_json 2>/dev/null)"; then
|
|
719
|
+
python3 - "$status_json" <<'PY'
|
|
720
|
+
import json
|
|
721
|
+
import sys
|
|
722
|
+
|
|
723
|
+
payload = json.loads(sys.argv[1])
|
|
724
|
+
status = payload.get("status") if isinstance(payload.get("status"), dict) else {}
|
|
725
|
+
print(status.get("live_session_count", "unknown"))
|
|
726
|
+
PY
|
|
727
|
+
else
|
|
728
|
+
printf '%s\n' "unknown"
|
|
729
|
+
fi
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
ptybroker_status_json() {
|
|
733
|
+
"$PYTHON3_BIN" - "$CURRENT_LINK" <<'PY'
|
|
734
|
+
import json
|
|
735
|
+
import sys
|
|
736
|
+
from pathlib import Path
|
|
737
|
+
|
|
738
|
+
current = Path(sys.argv[1])
|
|
739
|
+
sys.path.insert(0, str(current / "companiond"))
|
|
740
|
+
from pty_broker_client import PTYBrokerClient, ensure_pty_broker_token
|
|
741
|
+
|
|
742
|
+
companion = Path.home() / ".claude" / "companion"
|
|
743
|
+
client = PTYBrokerClient(companion / "pty-broker.sock", ensure_pty_broker_token(companion), timeout=1.0)
|
|
744
|
+
print(json.dumps({"ok": True, "status": client.status()}, sort_keys=True))
|
|
745
|
+
PY
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
ptybroker_desired_revision() {
|
|
749
|
+
python3 - "$CURRENT_LINK" <<'PY'
|
|
750
|
+
import json
|
|
751
|
+
import sys
|
|
752
|
+
from pathlib import Path
|
|
753
|
+
|
|
754
|
+
current = Path(sys.argv[1])
|
|
755
|
+
for path in [current / "manifest.json", current / "mac" / "SOURCE_REVISION", current / "SOURCE_REVISION"]:
|
|
756
|
+
try:
|
|
757
|
+
if path.name == "manifest.json":
|
|
758
|
+
print(json.loads(path.read_text()).get("source_revision") or "")
|
|
759
|
+
else:
|
|
760
|
+
print(path.read_text().strip())
|
|
761
|
+
raise SystemExit(0)
|
|
762
|
+
except FileNotFoundError:
|
|
763
|
+
continue
|
|
764
|
+
except Exception:
|
|
765
|
+
continue
|
|
766
|
+
print("")
|
|
767
|
+
PY
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
ptybroker_live_revision() {
|
|
771
|
+
python3 - "${1:-{}}" <<'PY'
|
|
772
|
+
import json
|
|
773
|
+
import sys
|
|
774
|
+
|
|
775
|
+
payload = json.loads(sys.argv[1])
|
|
776
|
+
status = payload.get("status") if isinstance(payload.get("status"), dict) else payload
|
|
777
|
+
print(status.get("source_revision") or "")
|
|
778
|
+
PY
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
ptybroker_deployment_state_json() {
|
|
782
|
+
python3 - "$CURRENT_LINK" "${1:-{}}" <<'PY'
|
|
783
|
+
import json
|
|
784
|
+
import os
|
|
785
|
+
import sys
|
|
786
|
+
from pathlib import Path
|
|
787
|
+
|
|
788
|
+
current = Path(sys.argv[1])
|
|
789
|
+
payload = json.loads(sys.argv[2])
|
|
790
|
+
live = payload.get("status") if isinstance(payload.get("status"), dict) else payload
|
|
791
|
+
|
|
792
|
+
def read_revision(root: Path):
|
|
793
|
+
for path in [root / "manifest.json", root / "mac" / "SOURCE_REVISION", root / "SOURCE_REVISION"]:
|
|
794
|
+
try:
|
|
795
|
+
if path.name == "manifest.json":
|
|
796
|
+
return json.loads(path.read_text()).get("source_revision")
|
|
797
|
+
value = path.read_text().strip()
|
|
798
|
+
return value or None
|
|
799
|
+
except FileNotFoundError:
|
|
800
|
+
continue
|
|
801
|
+
except Exception:
|
|
802
|
+
continue
|
|
803
|
+
return None
|
|
804
|
+
|
|
805
|
+
desired_root = current.resolve()
|
|
806
|
+
desired = {
|
|
807
|
+
"runtime_root": str(desired_root),
|
|
808
|
+
"script_path": str(desired_root / "companiond" / "pty_broker_service.py"),
|
|
809
|
+
"source_revision": read_revision(desired_root),
|
|
810
|
+
"protocol_version": 1,
|
|
811
|
+
}
|
|
812
|
+
reasons = []
|
|
813
|
+
live_root = live.get("runtime_root")
|
|
814
|
+
if live_root:
|
|
815
|
+
if os.path.realpath(str(live_root)) != str(desired_root):
|
|
816
|
+
reasons.append("runtime_root_mismatch")
|
|
817
|
+
else:
|
|
818
|
+
reasons.append("runtime_root_missing")
|
|
819
|
+
live_script = live.get("script_path")
|
|
820
|
+
if live_script:
|
|
821
|
+
if os.path.realpath(str(live_script)) != str(desired["script_path"]):
|
|
822
|
+
reasons.append("script_path_mismatch")
|
|
823
|
+
else:
|
|
824
|
+
reasons.append("script_path_missing")
|
|
825
|
+
live_revision = live.get("source_revision")
|
|
826
|
+
if desired["source_revision"] and not live_revision:
|
|
827
|
+
reasons.append("source_revision_missing")
|
|
828
|
+
elif live_revision and desired["source_revision"] and str(live_revision) != str(desired["source_revision"]):
|
|
829
|
+
reasons.append("source_revision_mismatch")
|
|
830
|
+
try:
|
|
831
|
+
live_protocol = int(live.get("protocol_version") or 0)
|
|
832
|
+
except (TypeError, ValueError):
|
|
833
|
+
live_protocol = 0
|
|
834
|
+
if live_protocol != desired["protocol_version"]:
|
|
835
|
+
if not live.get("protocol_version"):
|
|
836
|
+
reasons.append("protocol_version_missing")
|
|
837
|
+
else:
|
|
838
|
+
reasons.append("protocol_version_mismatch")
|
|
839
|
+
state = "current" if not reasons else "stale_deferred"
|
|
840
|
+
print(json.dumps({
|
|
841
|
+
"state": state,
|
|
842
|
+
"restart_deferred": state == "stale_deferred",
|
|
843
|
+
"reasons": reasons,
|
|
844
|
+
"desired": desired,
|
|
845
|
+
"live": live,
|
|
846
|
+
}, sort_keys=True))
|
|
847
|
+
PY
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
ptybroker_report_deferred_restart() {
|
|
851
|
+
local state_json
|
|
852
|
+
state_json="$(ptybroker_deployment_state_json "$1")"
|
|
853
|
+
python3 - "$state_json" <<'PY'
|
|
854
|
+
import json
|
|
855
|
+
import sys
|
|
856
|
+
|
|
857
|
+
state = json.loads(sys.argv[1])
|
|
858
|
+
if state.get("state") != "stale_deferred":
|
|
859
|
+
raise SystemExit(0)
|
|
860
|
+
live = state.get("live") if isinstance(state.get("live"), dict) else {}
|
|
861
|
+
desired = state.get("desired") if isinstance(state.get("desired"), dict) else {}
|
|
862
|
+
print(
|
|
863
|
+
"WARNING: ptybroker running older code; normal install preserved live PTYs; "
|
|
864
|
+
"broker restart is deferred; "
|
|
865
|
+
f"live_source_revision={live.get('source_revision')} "
|
|
866
|
+
f"desired_source_revision={desired.get('source_revision')} "
|
|
867
|
+
f"live_pid={live.get('pid')} "
|
|
868
|
+
f"live_session_count={live.get('live_session_count')}"
|
|
869
|
+
)
|
|
870
|
+
PY
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
ptybroker_state_field() {
|
|
874
|
+
python3 - "$1" "$2" <<'PY'
|
|
875
|
+
import json
|
|
876
|
+
import sys
|
|
877
|
+
|
|
878
|
+
payload = json.loads(sys.argv[1])
|
|
879
|
+
value = payload
|
|
880
|
+
for part in sys.argv[2].split("."):
|
|
881
|
+
if isinstance(value, dict):
|
|
882
|
+
value = value.get(part)
|
|
883
|
+
else:
|
|
884
|
+
value = None
|
|
885
|
+
print("" if value is None else value)
|
|
886
|
+
PY
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
ensure_ptybroker_agent() {
|
|
890
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
891
|
+
local rendered="$PLIST_BUILD_DIR/$PAIRLING_PTYBROKER_LABEL.plist"
|
|
892
|
+
local changed=0
|
|
893
|
+
if [[ ! -f "$PTYBROKER_USER_PLIST" ]] || ! cmp -s "$rendered" "$PTYBROKER_USER_PLIST"; then
|
|
894
|
+
cp "$rendered" "$PTYBROKER_USER_PLIST"
|
|
895
|
+
chmod 644 "$PTYBROKER_USER_PLIST"
|
|
896
|
+
changed=1
|
|
897
|
+
fi
|
|
898
|
+
if is_dry_run; then
|
|
899
|
+
if [[ "$changed" == "1" ]]; then
|
|
900
|
+
log "dry-run: rendered $PTYBROKER_USER_PLIST"
|
|
901
|
+
else
|
|
902
|
+
log "dry-run: $PTYBROKER_USER_PLIST unchanged"
|
|
903
|
+
fi
|
|
904
|
+
return
|
|
905
|
+
fi
|
|
906
|
+
if ! launchctl print "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1; then
|
|
907
|
+
launchctl bootstrap "gui/$(id -u)" "$PTYBROKER_USER_PLIST" >/dev/null 2>&1 || true
|
|
908
|
+
launchctl kickstart "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1 || true
|
|
909
|
+
return
|
|
910
|
+
fi
|
|
911
|
+
local status_json
|
|
912
|
+
if status_json="$(ptybroker_status_json 2>/dev/null)"; then
|
|
913
|
+
ptybroker_report_deferred_restart "$status_json"
|
|
914
|
+
else
|
|
915
|
+
log "WARNING: ptybroker status unreachable_socket; normal install preserved live PTYs but broker freshness is unknown; broker restart is deferred"
|
|
916
|
+
fi
|
|
917
|
+
if [[ "$changed" == "1" ]]; then
|
|
918
|
+
local live_count
|
|
919
|
+
live_count="$(ptybroker_live_session_count)"
|
|
920
|
+
log "ptybroker plist changed but broker is already loaded; preserving PTYs and deferring broker restart (live_sessions=$live_count)"
|
|
921
|
+
fi
|
|
922
|
+
if [[ ! -S "$HOME/.claude/companion/pty-broker.sock" ]]; then
|
|
923
|
+
launchctl kickstart "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1 || true
|
|
924
|
+
fi
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
reconcile_ptybroker() {
|
|
928
|
+
ensure_state
|
|
929
|
+
render_plists
|
|
930
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
931
|
+
cp "$PLIST_BUILD_DIR/$PAIRLING_PTYBROKER_LABEL.plist" "$PTYBROKER_USER_PLIST"
|
|
932
|
+
chmod 644 "$PTYBROKER_USER_PLIST"
|
|
933
|
+
if is_dry_run; then
|
|
934
|
+
log "dry-run: would reconcile $PAIRLING_PTYBROKER_LABEL"
|
|
935
|
+
return
|
|
936
|
+
fi
|
|
937
|
+
if ! launchctl print "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1; then
|
|
938
|
+
launchctl bootstrap "gui/$(id -u)" "$PTYBROKER_USER_PLIST" >/dev/null 2>&1 || true
|
|
939
|
+
launchctl kickstart "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1 || true
|
|
940
|
+
log "Started $PAIRLING_PTYBROKER_LABEL"
|
|
941
|
+
return
|
|
942
|
+
fi
|
|
943
|
+
local status_json state_json live_count live_pid
|
|
944
|
+
if ! status_json="$(ptybroker_status_json 2>/dev/null)"; then
|
|
945
|
+
log "ERROR: ptybroker is loaded but status RPC is unreachable; refusing reconcile until socket is reachable or broker is manually stopped." >&2
|
|
946
|
+
exit 1
|
|
947
|
+
fi
|
|
948
|
+
state_json="$(ptybroker_deployment_state_json "$status_json")"
|
|
949
|
+
live_count="$(ptybroker_state_field "$state_json" "live.live_session_count")"
|
|
950
|
+
live_pid="$(ptybroker_state_field "$state_json" "live.pid")"
|
|
951
|
+
if [[ "${live_count:-0}" != "0" ]]; then
|
|
952
|
+
log "ERROR: ptybroker restart deferred: live_session_count=$live_count live_pid=$live_pid; close/drain live PTYs before broker code can be updated." >&2
|
|
953
|
+
exit 1
|
|
954
|
+
fi
|
|
955
|
+
log "Operator requested idle ptybroker reconcile; restarting broker live_pid=$live_pid live_session_count=0"
|
|
956
|
+
launchctl bootout "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL" >/dev/null 2>&1 || true
|
|
957
|
+
launchctl bootout "gui/$(id -u)" "$PTYBROKER_USER_PLIST" >/dev/null 2>&1 || true
|
|
958
|
+
launchctl bootstrap "gui/$(id -u)" "$PTYBROKER_USER_PLIST" >/dev/null 2>&1 || true
|
|
959
|
+
launchctl kickstart -k "gui/$(id -u)/$PAIRLING_PTYBROKER_LABEL"
|
|
960
|
+
status_json="$(ptybroker_status_json)"
|
|
961
|
+
state_json="$(ptybroker_deployment_state_json "$status_json")"
|
|
962
|
+
if [[ "$(ptybroker_state_field "$state_json" "state")" != "current" ]]; then
|
|
963
|
+
log "ERROR: ptybroker restart completed but status is not current: $state_json" >&2
|
|
964
|
+
exit 1
|
|
965
|
+
fi
|
|
966
|
+
log "Reconciled $PAIRLING_PTYBROKER_LABEL with current runtime"
|
|
967
|
+
}
|
|
968
|
+
|
|
671
969
|
stop_user_agent() {
|
|
672
970
|
if is_dry_run; then
|
|
673
971
|
log "dry-run: would stop $PAIRLING_DAEMON_LABEL"
|
|
@@ -689,7 +987,7 @@ stop_connectd_agent() {
|
|
|
689
987
|
install_guardian_if_possible() {
|
|
690
988
|
local rendered="$PLIST_BUILD_DIR/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
691
989
|
if [[ "${PAIRLING_INSTALL_GUARDIAN:-0}" != "1" ]]; then
|
|
692
|
-
log "
|
|
990
|
+
log "Optional power guardian not installed; pairing can continue without the privileged sleep helper."
|
|
693
991
|
return
|
|
694
992
|
fi
|
|
695
993
|
if is_dry_run; then
|
|
@@ -709,7 +1007,7 @@ install_guardian_if_possible() {
|
|
|
709
1007
|
}
|
|
710
1008
|
|
|
711
1009
|
run_doctor() {
|
|
712
|
-
"$REPO_ROOT/mac/install/doctor.sh"
|
|
1010
|
+
"$REPO_ROOT/mac/install/doctor.sh"
|
|
713
1011
|
}
|
|
714
1012
|
|
|
715
1013
|
rollback() {
|
|
@@ -727,6 +1025,7 @@ rollback() {
|
|
|
727
1025
|
ln -s "$current_target" "$PREVIOUS_LINK"
|
|
728
1026
|
fi
|
|
729
1027
|
render_plists
|
|
1028
|
+
ensure_ptybroker_agent
|
|
730
1029
|
start_user_agent
|
|
731
1030
|
start_connectd_agent
|
|
732
1031
|
append_history "rollback" "rolled back to $previous_target"
|
|
@@ -735,13 +1034,14 @@ rollback() {
|
|
|
735
1034
|
|
|
736
1035
|
install_runtime() {
|
|
737
1036
|
log "Pairling setup preview:"
|
|
738
|
-
log " app support: $APP_SUPPORT"
|
|
739
|
-
log " logs: $LOGS_ROOT"
|
|
1037
|
+
log " app support: $(display_path "$APP_SUPPORT")"
|
|
1038
|
+
log " logs: $(display_path "$LOGS_ROOT")"
|
|
740
1039
|
log " LaunchAgent: $PAIRLING_DAEMON_LABEL"
|
|
1040
|
+
log " PTY Broker LaunchAgent: $PAIRLING_PTYBROKER_LABEL"
|
|
741
1041
|
log " Connect LaunchAgent: $PAIRLING_CONNECTD_LABEL"
|
|
742
1042
|
log " runtime port: $PAIRLING_RUNTIME_PORT"
|
|
743
|
-
log " old Pairling predecessor cleanup label: $LEGACY_DAEMON_LABEL"
|
|
744
1043
|
run_compile_checks
|
|
1044
|
+
run_psk_dependency_checks
|
|
745
1045
|
ensure_state
|
|
746
1046
|
copy_release
|
|
747
1047
|
switch_current
|
|
@@ -749,6 +1049,7 @@ install_runtime() {
|
|
|
749
1049
|
install_shell_wrapper
|
|
750
1050
|
render_plists
|
|
751
1051
|
unload_legacy_daemon
|
|
1052
|
+
ensure_ptybroker_agent
|
|
752
1053
|
start_user_agent
|
|
753
1054
|
start_connectd_agent
|
|
754
1055
|
install_guardian_if_possible
|
|
@@ -769,6 +1070,7 @@ start_runtime() {
|
|
|
769
1070
|
ensure_state
|
|
770
1071
|
render_plists
|
|
771
1072
|
unload_legacy_daemon
|
|
1073
|
+
ensure_ptybroker_agent
|
|
772
1074
|
start_user_agent
|
|
773
1075
|
start_connectd_agent
|
|
774
1076
|
log "Started $PAIRLING_DAEMON_LABEL"
|
|
@@ -877,6 +1179,11 @@ secret = str(
|
|
|
877
1179
|
)
|
|
878
1180
|
install_id = str(payload.get("install_id") or "")
|
|
879
1181
|
mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_name") or socket.gethostname())
|
|
1182
|
+
# WS3: the Mac ephemeral ECDH public key (base64url) from /pair/start. Carrying it in the
|
|
1183
|
+
# pair URL is what lets the phone run PSK-authenticated ECDH from the OUT-OF-BAND (QR/paste)
|
|
1184
|
+
# payload — the secret never goes on the wire. Without it the phone falls back to the legacy
|
|
1185
|
+
# plaintext claim, so this field is the bridge that actually makes WS3 engage.
|
|
1186
|
+
mac_ake_pub = str(payload.get("mac_ake_pub") or (payload.get("claim") or {}).get("mac_ake_pub") or "")
|
|
880
1187
|
|
|
881
1188
|
def detected_tailnet_ip() -> str:
|
|
882
1189
|
override = os.environ.get("PAIRLING_TEST_TAILSCALE_IP")
|
|
@@ -933,6 +1240,12 @@ if pair_id and secret:
|
|
|
933
1240
|
"pair_id": pair_id,
|
|
934
1241
|
"secret": secret,
|
|
935
1242
|
}
|
|
1243
|
+
if mac_ake_pub:
|
|
1244
|
+
# WS3: out-of-band delivery of the Mac ECDH key + protocol marker. The phone routes
|
|
1245
|
+
# to PSK-authenticated ECDH (secret never transmitted) when both are present; their
|
|
1246
|
+
# absence is the legacy plaintext claim. pv=2 == PSK protocol version.
|
|
1247
|
+
pair_params["mac_ake_pub"] = mac_ake_pub
|
|
1248
|
+
pair_params["pv"] = "2"
|
|
936
1249
|
if pair_route.get("source") == "pairling_connectd" and pair_route.get("status") == "ready":
|
|
937
1250
|
pair_params["route_source"] = "pairling_connectd"
|
|
938
1251
|
pair_params["route_status"] = "ready"
|
|
@@ -1228,6 +1541,7 @@ commands:
|
|
|
1228
1541
|
status
|
|
1229
1542
|
doctor --json
|
|
1230
1543
|
doctor --first-run --json
|
|
1544
|
+
reconcile-ptybroker
|
|
1231
1545
|
pair
|
|
1232
1546
|
connect-auth-open
|
|
1233
1547
|
devices
|
|
@@ -1270,6 +1584,9 @@ case "$cmd" in
|
|
|
1270
1584
|
doctor)
|
|
1271
1585
|
"$REPO_ROOT/mac/install/doctor.sh" "$@"
|
|
1272
1586
|
;;
|
|
1587
|
+
reconcile-ptybroker|--reconcile-ptybroker|--restart-ptybroker-if-idle)
|
|
1588
|
+
reconcile_ptybroker
|
|
1589
|
+
;;
|
|
1273
1590
|
pair)
|
|
1274
1591
|
pair_runtime "$@"
|
|
1275
1592
|
;;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
parser = argparse.ArgumentParser(description="Verify Pairling PSK pairing import dependencies.")
|
|
12
|
+
parser.add_argument("companiond_path")
|
|
13
|
+
parser.add_argument("--label", default="dependency check")
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
companiond = Path(args.companiond_path).resolve()
|
|
17
|
+
sys.path.insert(0, str(companiond))
|
|
18
|
+
os.environ.pop("PAIRLING_PSK_REQUIRED", None)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import cryptography # noqa: F401
|
|
22
|
+
import pairling_psk # noqa: F401
|
|
23
|
+
import pairling_pairing # noqa: F401
|
|
24
|
+
except Exception as exc:
|
|
25
|
+
print(
|
|
26
|
+
"PSK pairing dependency check failed during "
|
|
27
|
+
f"{args.label}: companiond_path={companiond}; Python must import "
|
|
28
|
+
"cryptography, pairling_psk, and pairling_pairing while "
|
|
29
|
+
"PAIRLING_PSK_REQUIRED is default-on; otherwise PSK pairing is "
|
|
30
|
+
"unavailable/fail-closed and daemon liveness alone is insufficient "
|
|
31
|
+
"(pairing endpoints may return pairing_unavailable). "
|
|
32
|
+
f"Cause: {type(exc).__name__}: {exc}",
|
|
33
|
+
file=sys.stderr,
|
|
34
|
+
)
|
|
35
|
+
return 1
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
raise SystemExit(main())
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
PAIRLING_DAEMON_LABEL = "dev.pairling.companiond"
|
|
11
11
|
PAIRLING_GUARDIAN_LABEL = "dev.pairling.power-guardian"
|
|
12
12
|
PAIRLING_CONNECTD_LABEL = "dev.pairling.connectd"
|
|
13
|
+
PAIRLING_PTYBROKER_LABEL = "dev.pairling.ptybroker"
|
|
13
14
|
PAIRLING_RUNTIME_PORT = "7773"
|
|
14
15
|
|
|
15
16
|
|
|
@@ -95,6 +96,27 @@ def connectd_plist(current: Path, logs: Path) -> dict:
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def ptybroker_plist(current: Path, logs: Path, python_bin: str) -> dict:
|
|
100
|
+
app_support = current.parent.parent
|
|
101
|
+
return {
|
|
102
|
+
"Label": PAIRLING_PTYBROKER_LABEL,
|
|
103
|
+
"ProgramArguments": [
|
|
104
|
+
python_bin,
|
|
105
|
+
str(current / "companiond" / "pty_broker_service.py"),
|
|
106
|
+
],
|
|
107
|
+
"EnvironmentVariables": {
|
|
108
|
+
"PAIRLING_APP_SUPPORT_ROOT": str(app_support),
|
|
109
|
+
"PAIRLING_LOGS_ROOT": str(logs),
|
|
110
|
+
"PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
111
|
+
},
|
|
112
|
+
"RunAtLoad": True,
|
|
113
|
+
"KeepAlive": True,
|
|
114
|
+
"ThrottleInterval": 10,
|
|
115
|
+
"StandardOutPath": str(logs / "ptybroker.log"),
|
|
116
|
+
"StandardErrorPath": str(logs / "ptybroker.err"),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
98
120
|
def main() -> int:
|
|
99
121
|
parser = argparse.ArgumentParser()
|
|
100
122
|
parser.add_argument("--current-root", required=True)
|
|
@@ -110,6 +132,7 @@ def main() -> int:
|
|
|
110
132
|
out = Path(args.output_dir)
|
|
111
133
|
|
|
112
134
|
write_plist(out / f"{PAIRLING_DAEMON_LABEL}.plist", daemon_plist(current, logs, args.daemon_python))
|
|
135
|
+
write_plist(out / f"{PAIRLING_PTYBROKER_LABEL}.plist", ptybroker_plist(current, logs, args.daemon_python))
|
|
113
136
|
write_plist(out / f"{PAIRLING_GUARDIAN_LABEL}.plist", guardian_plist(current, logs, args.guardian_python))
|
|
114
137
|
write_plist(out / f"{PAIRLING_CONNECTD_LABEL}.plist", connectd_plist(current, logs))
|
|
115
138
|
return 0
|
|
@@ -4,14 +4,13 @@ set -euo pipefail
|
|
|
4
4
|
PAIRLING_DAEMON_LABEL="dev.pairling.companiond"
|
|
5
5
|
PAIRLING_GUARDIAN_LABEL="dev.pairling.power-guardian"
|
|
6
6
|
PAIRLING_CONNECTD_LABEL="dev.pairling.connectd"
|
|
7
|
-
|
|
7
|
+
PAIRLING_PTYBROKER_LABEL="dev.pairling.ptybroker"
|
|
8
8
|
APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
|
|
9
9
|
LOGS_ROOT="${PAIRLING_LOGS_ROOT:-${COMPANION_LOGS_ROOT:-$HOME/Library/Logs/Pairling}}"
|
|
10
10
|
USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
|
|
11
11
|
CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
|
|
12
|
-
|
|
12
|
+
PTYBROKER_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_PTYBROKER_LABEL.plist"
|
|
13
13
|
SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
|
|
14
|
-
LEGACY_SYSTEM_PLIST="/Library/LaunchDaemons/com.mghome.companion-power-guardian.plist"
|
|
15
14
|
YES="false"
|
|
16
15
|
DELETE_STATE="false"
|
|
17
16
|
DELETE_LOGS="false"
|
|
@@ -103,19 +102,12 @@ confirm
|
|
|
103
102
|
|
|
104
103
|
bootout_user "$PAIRLING_DAEMON_LABEL" "$USER_PLIST"
|
|
105
104
|
bootout_user "$PAIRLING_CONNECTD_LABEL" "$CONNECTD_USER_PLIST"
|
|
105
|
+
bootout_user "$PAIRLING_PTYBROKER_LABEL" "$PTYBROKER_USER_PLIST"
|
|
106
106
|
rm -f "$USER_PLIST"
|
|
107
107
|
rm -f "$CONNECTD_USER_PLIST"
|
|
108
|
+
rm -f "$PTYBROKER_USER_PLIST"
|
|
108
109
|
bootout_system "$PAIRLING_GUARDIAN_LABEL" "$SYSTEM_PLIST"
|
|
109
110
|
|
|
110
|
-
if [[ -f "$LEGACY_USER_PLIST" ]]; then
|
|
111
|
-
bootout_user "$LEGACY_DAEMON_LABEL" "$LEGACY_USER_PLIST"
|
|
112
|
-
printf 'Warning: legacy LaunchAgent still exists and was unloaded if possible: %s\n' "$LEGACY_USER_PLIST"
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
if [[ -f "$LEGACY_SYSTEM_PLIST" ]]; then
|
|
116
|
-
printf 'Warning: legacy guardian LaunchDaemon still exists: %s\n' "$LEGACY_SYSTEM_PLIST"
|
|
117
|
-
fi
|
|
118
|
-
|
|
119
111
|
rm -rf "$APP_SUPPORT/pair" 2>/dev/null || true
|
|
120
112
|
|
|
121
113
|
if [[ "$DELETE_STATE" == "true" ]]; then
|