pairling 0.2.7 → 0.2.8

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.
@@ -51,7 +51,6 @@ 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
- PAIRLING_MINTD_LABEL="dev.pairling.mintd"
55
54
  APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
56
55
  RUNTIME_ROOT="$APP_SUPPORT/runtime"
57
56
  RELEASES_ROOT="$RUNTIME_ROOT/releases"
@@ -71,12 +70,6 @@ USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
71
70
  CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
72
71
  PTYBROKER_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_PTYBROKER_LABEL.plist"
73
72
  SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
74
- MINTD_SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_MINTD_LABEL.plist"
75
- MINTD_SYSTEM_ROOT="/Library/Application Support/Pairling"
76
- MINTD_SECRET_DIR="$MINTD_SYSTEM_ROOT/mint"
77
- MINTD_RUN_DIR="$MINTD_SYSTEM_ROOT/run/mintd"
78
- MINTD_SYSTEM_BINARY="$MINTD_SECRET_DIR/pairling-tailnet-mintd"
79
- MINTD_LOGS_DIR="/Library/Logs/Pairling"
80
73
  MCP_SERVER_DIR="$HOME/.claude/mcp-servers"
81
74
  MCP_SERVER_SHIM="$MCP_SERVER_DIR/phone-tools.py"
82
75
  PYTHON3_BIN="${PAIRLING_DAEMON_PYTHON:-${COMPANION_DAEMON_PYTHON:-$(command -v python3)}}"
@@ -313,13 +306,12 @@ copy_release() {
313
306
  cp "$REPO_ROOT/mac/guardian/companion-power-guardian.py" "$tmp/guardian/"
314
307
  cp "$REPO_ROOT/mac/guardian/guardian_contract.py" "$tmp/guardian/"
315
308
  build_connectd_binary "$tmp/connectd/pairling-connectd"
316
- build_mintd_binary "$tmp/connectd/pairling-tailnet-mintd"
317
309
  stage_vendored_python "$tmp/python"
318
310
  run_staged_psk_dependency_checks "$tmp"
319
- copy_runtime_source_tree "$tmp/mac" "$tmp/connectd/pairling-connectd" "$tmp/connectd/pairling-tailnet-mintd"
311
+ copy_runtime_source_tree "$tmp/mac" "$tmp/connectd/pairling-connectd"
320
312
  write_installed_pairling_launcher "$tmp/bin/pairling"
321
313
  chmod 755 "$tmp/bin/pairling" "$tmp/companiond/pairlingd.py" "$tmp/mcp/phone_tools.py" "$tmp/guardian/companion-power-guardian.py"
322
- chmod 755 "$tmp/connectd/pairling-connectd" "$tmp/connectd/pairling-tailnet-mintd"
314
+ chmod 755 "$tmp/connectd/pairling-connectd"
323
315
  chmod 644 "$tmp/companiond/"*.py "$tmp/mcp/"*.py "$tmp/guardian/"*.py
324
316
  chmod 644 "$tmp/companiond/providers/"*.py
325
317
  chmod 644 "$tmp/companiond/integrations/"*.py "$tmp/companiond/integrations/aperture_cli/"*.py
@@ -333,7 +325,6 @@ copy_release() {
333
325
  copy_runtime_source_tree() {
334
326
  local mac_root="$1"
335
327
  local connectd_binary="$2"
336
- local mintd_binary="$3"
337
328
  mkdir -p \
338
329
  "$mac_root/companiond" \
339
330
  "$mac_root/companiond/providers" \
@@ -360,13 +351,12 @@ copy_runtime_source_tree() {
360
351
  cp -R "$REPO_ROOT/mac/connectd/cmd" "$mac_root/connectd/"
361
352
  cp -R "$REPO_ROOT/mac/connectd/internal" "$mac_root/connectd/"
362
353
  cp "$connectd_binary" "$mac_root/connectd/bin/pairling-connectd"
363
- cp "$mintd_binary" "$mac_root/connectd/bin/pairling-tailnet-mintd"
364
354
  cp "$REPO_ROOT/mac/guardian/"*.py "$mac_root/guardian/"
365
355
  cp "$REPO_ROOT/mac/install/"*.sh "$mac_root/install/"
366
356
  cp "$REPO_ROOT/mac/install/"*.py "$mac_root/install/"
367
357
  cp "$REPO_ROOT/mac/mcp/"*.py "$mac_root/mcp/"
368
358
  cp "$REPO_ROOT/mac/packaging/bin/pairling" "$mac_root/packaging/bin/"
369
- chmod 755 "$mac_root/connectd/bin/pairling-connectd" "$mac_root/connectd/bin/pairling-tailnet-mintd" "$mac_root/install/"*.sh "$mac_root/mcp/phone_tools.py" "$mac_root/packaging/bin/pairling"
359
+ chmod 755 "$mac_root/connectd/bin/pairling-connectd" "$mac_root/install/"*.sh "$mac_root/mcp/phone_tools.py" "$mac_root/packaging/bin/pairling"
370
360
  chmod 644 "$mac_root/VERSION" "$mac_root/SOURCE_REVISION" "$mac_root/SOURCE_BRANCH" "$mac_root/SOURCE_DIRTY"
371
361
  }
372
362
 
@@ -490,57 +480,6 @@ build_connectd_binary() {
490
480
  )
491
481
  }
492
482
 
493
- build_mintd_binary() {
494
- local out="$1"
495
- local prebuilt_env="${PAIRLING_MINTD_PREBUILT:-}"
496
- if [[ -n "$prebuilt_env" ]]; then
497
- if [[ ! -f "$prebuilt_env" ]]; then
498
- log "ERROR: PAIRLING_MINTD_PREBUILT points at a missing file: $prebuilt_env" >&2
499
- exit 1
500
- fi
501
- local required_team="${PAIRLING_MINTD_TEAM_ID:-${PAIRLING_CONNECTD_TEAM_ID:-965AVD34A3}}"
502
- if [[ "$required_team" != "-" ]]; then
503
- if ! /usr/bin/codesign --verify --strict "$prebuilt_env" >/dev/null 2>&1; then
504
- log "ERROR: mintd binary failed codesign verification; refusing to stage: $prebuilt_env" >&2
505
- exit 1
506
- fi
507
- local team
508
- team="$(/usr/bin/codesign -dvv "$prebuilt_env" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
509
- if [[ "$team" != "$required_team" ]]; then
510
- log "ERROR: mintd binary TeamIdentifier '${team:-none}' does not match required '$required_team'; refusing to stage: $prebuilt_env" >&2
511
- exit 1
512
- fi
513
- fi
514
- cp "$prebuilt_env" "$out"
515
- chmod 755 "$out"
516
- return
517
- fi
518
- local prebuilt="$REPO_ROOT/mac/connectd/bin/pairling-tailnet-mintd"
519
- if [[ -x "$prebuilt" ]]; then
520
- cp "$prebuilt" "$out"
521
- chmod 755 "$out"
522
- return
523
- fi
524
- local go_bin
525
- go_bin="$(command -v go || true)"
526
- if [[ -z "$go_bin" ]]; then
527
- for candidate in /opt/homebrew/bin/go /usr/local/go/bin/go /usr/local/bin/go; do
528
- if [[ -x "$candidate" ]]; then
529
- go_bin="$candidate"
530
- break
531
- fi
532
- done
533
- fi
534
- if [[ -z "$go_bin" ]]; then
535
- log "ERROR: go is required to build pairling-tailnet-mintd" >&2
536
- exit 1
537
- fi
538
- (
539
- cd "$REPO_ROOT/mac/connectd"
540
- "$go_bin" build -o "$out" ./cmd/pairling-tailnet-mintd
541
- )
542
- }
543
-
544
483
  write_manifest() {
545
484
  local root="$1"
546
485
  python3 - "$REPO_ROOT" "$root" "$VERSION" "$REVISION" "$BRANCH" "$SOURCE_DIRTY" "$APP_SUPPORT" "$LOGS_ROOT" "$DEVICES_DB" "$PAIRLING_RUNTIME_PORT" <<'PY'
@@ -596,7 +535,6 @@ for rel in [
596
535
  "companiond/providers/external.py",
597
536
  "companiond/providers/registry.py",
598
537
  "connectd/pairling-connectd",
599
- "connectd/pairling-tailnet-mintd",
600
538
  "mcp/phone_tools.py",
601
539
  "guardian/companion-power-guardian.py",
602
540
  "guardian/guardian_contract.py",
@@ -629,7 +567,6 @@ manifest = {
629
567
  "daemon_label": "dev.pairling.companiond",
630
568
  "ptybroker_label": "dev.pairling.ptybroker",
631
569
  "connectd_label": "dev.pairling.connectd",
632
- "mintd_label": "dev.pairling.mintd",
633
570
  "guardian_label": "dev.pairling.power-guardian",
634
571
  },
635
572
  "paths": {
@@ -753,19 +690,6 @@ SH
753
690
  mv "$tmp" "$target"
754
691
  }
755
692
 
756
- mintd_provisioned() {
757
- # Architecture B (minting) is enabled in the companiond env once the
758
- # separate-uid mint broker has been installed by the explicit, consent-gated
759
- # `pairling enable-silent-join` flow. The broker's LaunchDaemon plist lives in
760
- # /Library/LaunchDaemons (root:wheel 0644, world-readable), so this steady-
761
- # state check needs no elevated privileges, and a plain setup never prompts
762
- # for a password. Architecture A (browser-login) stays the fallback whenever the
763
- # broker is absent; the credential gate is enforced at install time by
764
- # enable_silent_join + install_mintd_if_possible.
765
- if is_dry_run; then return 1; fi
766
- [[ -f "$MINTD_SYSTEM_PLIST" ]]
767
- }
768
-
769
693
  render_plists() {
770
694
  # Prefer the staged vendored interpreter whenever it exists, so start/
771
695
  # rollback (which don't re-stage) also run the daemon under dev.pairling.python.
@@ -780,9 +704,6 @@ render_plists() {
780
704
  --daemon-python "$daemon_python"
781
705
  --guardian-python "$GUARDIAN_PYTHON_BIN"
782
706
  )
783
- if mintd_provisioned; then
784
- render_args+=(--mint-enabled)
785
- fi
786
707
  python3 "$REPO_ROOT/mac/install/render-launchd.py" "${render_args[@]}"
787
708
  }
788
709
 
@@ -1148,16 +1069,6 @@ stop_connectd_agent() {
1148
1069
  launchctl bootout "gui/$(id -u)" "$CONNECTD_USER_PLIST" >/dev/null 2>&1 || true
1149
1070
  }
1150
1071
 
1151
- stop_mintd_daemon() {
1152
- if is_dry_run; then
1153
- log "dry-run: would stop $PAIRLING_MINTD_LABEL"
1154
- return
1155
- fi
1156
- if sudo -n true >/dev/null 2>&1; then
1157
- sudo launchctl bootout system "$MINTD_SYSTEM_PLIST" >/dev/null 2>&1 || true
1158
- fi
1159
- }
1160
-
1161
1072
  install_guardian_if_possible() {
1162
1073
  local rendered="$PLIST_BUILD_DIR/$PAIRLING_GUARDIAN_LABEL.plist"
1163
1074
  if [[ "${PAIRLING_INSTALL_GUARDIAN:-0}" != "1" ]]; then
@@ -1180,176 +1091,6 @@ install_guardian_if_possible() {
1180
1091
  fi
1181
1092
  }
1182
1093
 
1183
- mintd_uid_in_range() {
1184
- local uid="$1"
1185
- [[ "$uid" =~ ^[0-9]+$ && "$uid" -ge 450 && "$uid" -le 499 ]]
1186
- }
1187
-
1188
- ensure_mintd_service_account() {
1189
- if dscl . -read /Users/_pairling_mint >/dev/null 2>&1; then
1190
- local uid real
1191
- uid="$(dscl . -read /Users/_pairling_mint UniqueID | awk '{print $2}')"
1192
- real="$(dscl . -read /Users/_pairling_mint RealName 2>/dev/null | sed '1d;s/^ //')"
1193
- if ! mintd_uid_in_range "$uid"; then
1194
- log "Skipping mintd install: _pairling_mint UID $uid is outside 450-499." >&2
1195
- return 1
1196
- fi
1197
- if [[ "$real" != "Pairling Mint Broker" ]]; then
1198
- log "Skipping mintd install: _pairling_mint RealName is not Pairling Mint Broker." >&2
1199
- return 1
1200
- fi
1201
- return 0
1202
- fi
1203
- local uid=""
1204
- for candidate in $(seq 450 499); do
1205
- if ! dscl . -list /Users UniqueID | awk -v uid="$candidate" '$2 == uid {found=1} END {exit found ? 0 : 1}'; then
1206
- uid="$candidate"
1207
- break
1208
- fi
1209
- done
1210
- if [[ -z "$uid" ]]; then
1211
- log "Skipping mintd install: no free macOS role-account UID in 450-499." >&2
1212
- return 1
1213
- fi
1214
- local pw
1215
- pw="$(uuidgen)-$(uuidgen)"
1216
- sudo sysadminctl -addUser _pairling_mint -fullName "Pairling Mint Broker" -UID "$uid" -GID 20 -shell /usr/bin/false -home /var/empty -password "$pw" -roleAccount >/dev/null
1217
- unset pw
1218
- }
1219
-
1220
- install_mintd_if_possible() {
1221
- local rendered="$PLIST_BUILD_DIR/$PAIRLING_MINTD_LABEL.plist"
1222
- local mintd_secret="$MINTD_SECRET_DIR/client_secret.json"
1223
- if is_dry_run; then
1224
- log "dry-run: would install $PAIRLING_MINTD_LABEL when privileged setup is available"
1225
- return
1226
- fi
1227
- if ! sudo -n true >/dev/null 2>&1; then
1228
- log "Skipping mintd install: passwordless sudo is unavailable. Architecture A fallback remains available."
1229
- return
1230
- fi
1231
- if ! sudo test -f "$mintd_secret"; then
1232
- log "Skipping mintd install: OAuth client secret is not provisioned at $mintd_secret."
1233
- return
1234
- fi
1235
- ensure_mintd_service_account || return
1236
- sudo chmod 0600 "$mintd_secret"
1237
- sudo chown _pairling_mint:staff "$mintd_secret"
1238
- sudo install -d -m 0700 -o _pairling_mint -g staff "$MINTD_SECRET_DIR"
1239
- sudo install -d -m 0750 -o _pairling_mint -g staff "$MINTD_RUN_DIR"
1240
- sudo install -d -m 0750 -o _pairling_mint -g staff "$MINTD_LOGS_DIR"
1241
- sudo install -m 0755 -o root -g wheel "$CURRENT_LINK/connectd/pairling-tailnet-mintd" "$MINTD_SYSTEM_BINARY"
1242
- sudo cp "$rendered" "$MINTD_SYSTEM_PLIST"
1243
- sudo chown root:wheel "$MINTD_SYSTEM_PLIST"
1244
- sudo chmod 644 "$MINTD_SYSTEM_PLIST"
1245
- sudo launchctl bootout system "$MINTD_SYSTEM_PLIST" >/dev/null 2>&1 || true
1246
- sudo launchctl bootstrap system "$MINTD_SYSTEM_PLIST" >/dev/null 2>&1 || true
1247
- sudo launchctl kickstart -k "system/$PAIRLING_MINTD_LABEL"
1248
- }
1249
-
1250
- enable_silent_join() {
1251
- local client_secret_path=""
1252
- local assume_yes="0"
1253
- while [[ $# -gt 0 ]]; do
1254
- case "$1" in
1255
- --client-secret) shift; client_secret_path="${1:-}" ;;
1256
- --yes) assume_yes="1" ;;
1257
- --help|-h) log "usage: pairling enable-silent-join [--client-secret PATH] [--yes]"; return 0 ;;
1258
- *) log "usage: pairling enable-silent-join [--client-secret PATH] [--yes]" >&2; exit 2 ;;
1259
- esac
1260
- shift
1261
- done
1262
-
1263
- if is_dry_run; then
1264
- log "dry-run: would explain the mint broker, request one-time consent, and install $PAIRLING_MINTD_LABEL under interactive sudo"
1265
- return 0
1266
- fi
1267
-
1268
- if [[ ! -x "$CURRENT_LINK/connectd/pairling-tailnet-mintd" ]]; then
1269
- log "ERROR: the mint broker binary is not staged. Run 'pairling setup' first." >&2
1270
- exit 1
1271
- fi
1272
-
1273
- cat <<EOF
1274
-
1275
- Enable silent tailnet join (Architecture B)
1276
- -------------------------------------------
1277
- This installs a small background service, the mint broker ($PAIRLING_MINTD_LABEL).
1278
- It runs under its own macOS account (_pairling_mint), not as the Pairling daemon
1279
- and not as you. It holds your Tailscale OAuth client secret so the Pairling
1280
- daemon never can: the daemon may ask it for one short-lived, single-use phone
1281
- key per pairing, and nothing more.
1282
-
1283
- Installing a system service and a dedicated account needs administrator rights,
1284
- so you approve this one time now. After that, every future pairing joins your
1285
- tailnet silently, with no browser step.
1286
-
1287
- You provide a Tailscale OAuth client (scope auth_keys, tag tag:pairling-phone)
1288
- that you create in your own Tailscale admin console. The secret is stored
1289
- readable only by _pairling_mint, is never committed, and is never read by the
1290
- Pairling daemon. If you skip this, pairing still works over the browser path
1291
- (Architecture A); silent join stays off.
1292
-
1293
- EOF
1294
-
1295
- local secret_json=""
1296
- if [[ -n "$client_secret_path" ]]; then
1297
- [[ -f "$client_secret_path" ]] || { log "ERROR: --client-secret file not found: $client_secret_path" >&2; exit 1; }
1298
- secret_json="$(cat "$client_secret_path")"
1299
- elif [[ -t 0 ]]; then
1300
- log "Paste the Tailscale OAuth client JSON (with client_id and client_secret), then press Ctrl-D:"
1301
- secret_json="$(cat)"
1302
- else
1303
- log "ERROR: no Tailscale OAuth client provided. Re-run with --client-secret PATH (a JSON file with client_id and client_secret)." >&2
1304
- exit 1
1305
- fi
1306
-
1307
- if ! printf '%s' "$secret_json" | python3 -c 'import json,sys
1308
- d = json.load(sys.stdin)
1309
- sys.exit(0 if isinstance(d, dict) and d.get("client_id") and d.get("client_secret") else 1)' >/dev/null 2>&1; then
1310
- log "ERROR: the provided credential is not valid JSON with non-empty client_id and client_secret." >&2
1311
- exit 1
1312
- fi
1313
-
1314
- if [[ "$assume_yes" != "1" ]]; then
1315
- if [[ ! -t 0 ]]; then
1316
- log "ERROR: enabling silent join needs explicit consent. Re-run with --yes to confirm." >&2
1317
- exit 1
1318
- fi
1319
- printf 'Install the mint broker and enable silent join now? [y/N] '
1320
- local reply=""
1321
- read -r reply
1322
- case "$reply" in
1323
- y|Y|yes|YES) : ;;
1324
- *) log "Silent join not enabled. Pairing continues over the browser path."; return 0 ;;
1325
- esac
1326
- fi
1327
-
1328
- if ! sudo -v; then
1329
- log "Administrator approval was not granted. Silent join stays off; pairing still works over the browser path." >&2
1330
- exit 1
1331
- fi
1332
-
1333
- local mintd_secret="$MINTD_SECRET_DIR/client_secret.json"
1334
- local tmp_secret
1335
- tmp_secret="$(mktemp)"
1336
- chmod 600 "$tmp_secret"
1337
- printf '%s' "$secret_json" > "$tmp_secret"
1338
- sudo install -d -m 0700 "$MINTD_SECRET_DIR"
1339
- sudo install -m 0600 "$tmp_secret" "$mintd_secret"
1340
- rm -f "$tmp_secret"
1341
-
1342
- install_mintd_if_possible
1343
- if [[ ! -f "$MINTD_SYSTEM_PLIST" ]]; then
1344
- log "ERROR: the mint broker did not install. Silent join is not enabled; pairing still works over the browser path." >&2
1345
- exit 1
1346
- fi
1347
-
1348
- render_plists
1349
- start_user_agent
1350
- log "Silent tailnet join is enabled. Run 'pairling pair --qr' to pair; future pairings join your tailnet with no browser step."
1351
- }
1352
-
1353
1094
  run_doctor() {
1354
1095
  "$REPO_ROOT/mac/install/doctor.sh"
1355
1096
  }
@@ -1383,7 +1124,6 @@ install_runtime() {
1383
1124
  log " LaunchAgent: $PAIRLING_DAEMON_LABEL"
1384
1125
  log " PTY Broker LaunchAgent: $PAIRLING_PTYBROKER_LABEL"
1385
1126
  log " Connect LaunchAgent: $PAIRLING_CONNECTD_LABEL"
1386
- log " Mint LaunchDaemon: $PAIRLING_MINTD_LABEL"
1387
1127
  log " runtime port: $PAIRLING_RUNTIME_PORT"
1388
1128
  run_compile_checks
1389
1129
  run_psk_dependency_checks
@@ -1404,12 +1144,11 @@ install_runtime() {
1404
1144
  run_doctor || true
1405
1145
  fi
1406
1146
  log "Installed Pairling runtime $RELEASE_NAME"
1407
- if ! mintd_provisioned; then
1408
- log ""
1409
- log "Silent tailnet join (no browser step) is available but not yet enabled."
1410
- log "Turn it on once, using your own Tailscale account: pairling enable-silent-join"
1411
- fi
1412
1147
  if ! is_dry_run; then
1148
+ # Kick off the Tailscale sign-in before the QR so the pairing code can
1149
+ # advertise the now-ready Pairling Connect tailnet route instead of
1150
+ # downgrading to LAN/Bonjour. Never blocks or fails setup.
1151
+ auto_open_connect_auth
1413
1152
  log ""
1414
1153
  if ! PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS="${PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS:-35}" pair_runtime --qr; then
1415
1154
  log "Pairling installed, but setup could not generate a pairing invitation. Run: pairling doctor --json; pairling pair --qr" >&2
@@ -1432,7 +1171,6 @@ start_runtime() {
1432
1171
  }
1433
1172
 
1434
1173
  stop_runtime() {
1435
- stop_mintd_daemon
1436
1174
  stop_connectd_agent
1437
1175
  stop_user_agent
1438
1176
  log "Stopped $PAIRLING_DAEMON_LABEL"
@@ -1542,9 +1280,6 @@ mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_n
1542
1280
  # payload — the secret never goes on the wire. Without it the phone falls back to the legacy
1543
1281
  # plaintext claim, so this field is the bridge that actually makes WS3 engage.
1544
1282
  mac_ake_pub = str(payload.get("mac_ake_pub") or (payload.get("claim") or {}).get("mac_ake_pub") or "")
1545
- # The daemon computes pv authoritatively (3 when minting is enabled server-side,
1546
- # 2 for PSK-only). Read it from the same top-level-or-claim shape as mac_ake_pub.
1547
- claim_pv = str(payload.get("pv") or (payload.get("claim") or {}).get("pv") or "")
1548
1283
 
1549
1284
  def is_ats_local_ipv4(value: str) -> bool:
1550
1285
  try:
@@ -1664,15 +1399,10 @@ if pair_id and secret:
1664
1399
  if mac_ake_pub:
1665
1400
  # WS3: out-of-band delivery of the Mac ECDH key + protocol marker. The phone routes
1666
1401
  # to PSK-authenticated ECDH (secret never transmitted) when both are present; their
1667
- # absence is the legacy plaintext claim. pv=3 requests the B sealed-authkey
1668
- # extension when minting is available; pv=2 remains the PSK-only marker.
1402
+ # absence is the legacy plaintext claim. pv=2 is the PSK-only marker.
1669
1403
  pair_params["mac_ake_pub"] = mac_ake_pub
1670
- # Prefer the daemon's authoritative claim.pv so the QR can't downgrade to
1671
- # pv=2 when the CLI shell lacks PAIRLING_MINT_ENABLED (the daemon has the
1672
- # mint state, the CLI env may not). Fall back to the env marker only for a
1673
- # legacy daemon that does not advertise pv.
1674
- mint_enabled = os.environ.get("PAIRLING_MINT_ENABLED", "").strip().lower() in {"1", "true", "yes"}
1675
- pair_params["pv"] = claim_pv if claim_pv in {"2", "3"} else ("3" if mint_enabled else "2")
1404
+ # pv is always 2 when the Mac ECDH key is present (PSK-authenticated ECDH).
1405
+ pair_params["pv"] = "2"
1676
1406
  if pair_route.get("source") == "pairling_connectd" and pair_route.get("status") == "ready":
1677
1407
  pair_params["route_source"] = "pairling_connectd"
1678
1408
  pair_params["route_status"] = "ready"
@@ -1683,15 +1413,6 @@ if pair_id and secret:
1683
1413
  pair_params["route_status"] = "degraded"
1684
1414
  pair_params["route_kind"] = str(pair_route.get("kind") or pair_route.get("source") or "local")
1685
1415
  pair_params["route_contract"] = "pairling-runtime-v1"
1686
- # D1: carry the silent-join capability so the phone can warn before the QR is
1687
- # consumed. Only emitted when unavailable (e.g. under tailnet lock); the iOS
1688
- # parser defaults to available when the param is absent.
1689
- claim_block = payload.get("claim") or {}
1690
- if claim_block.get("silent_join_available") is False:
1691
- pair_params["silent_join_available"] = "false"
1692
- reason = str(claim_block.get("silent_join_unavailable_reason") or "")
1693
- if reason:
1694
- pair_params["silent_join_unavailable_reason"] = reason
1695
1416
  manual = {
1696
1417
  "base_url": base_url,
1697
1418
  "pair_id": pair_id,
@@ -1891,6 +1612,109 @@ connect_auth_open() {
1891
1612
  exit 1
1892
1613
  }
1893
1614
 
1615
+ # auto_open_connect_auth — kicked off by install_runtime BEFORE the pairing QR.
1616
+ # Polls connectd /status for readiness (reusing fetch_connectd_status, the same
1617
+ # helper pair_runtime imports). When connectd is in interactive mode and not yet
1618
+ # authenticated (auth_url_present == true AND auth_state != "authenticated"), it
1619
+ # POSTs http://127.0.0.1:7774/auth/open directly so connectd opens the browser
1620
+ # sign-in server-side. We POST directly (not via connect_auth_open, which ends in
1621
+ # `exit` and would terminate setup before the QR). Already-authenticated or
1622
+ # already-tagged connectd is skipped silently. This never fails setup and never
1623
+ # blocks indefinitely: the bounded poll falls through to pair_runtime regardless.
1624
+ auto_open_connect_auth() {
1625
+ if is_dry_run; then
1626
+ return 0
1627
+ fi
1628
+ python3 - "$REPO_ROOT" <<'PY' || true
1629
+ import json
1630
+ import os
1631
+ import sys
1632
+ import time
1633
+ import urllib.error
1634
+ import urllib.request
1635
+
1636
+ repo_root = sys.argv[1]
1637
+ sys.path.insert(0, os.path.join(repo_root, "mac", "companiond"))
1638
+ from pairling_connectd_status import fetch_connectd_status
1639
+
1640
+ AUTH_OPEN_URL = "http://127.0.0.1:7774/auth/open"
1641
+
1642
+
1643
+ def readiness_wait_seconds() -> float:
1644
+ try:
1645
+ return min(max(float(os.environ.get("PAIRLING_CONNECTD_AUTH_WAIT_SECONDS") or "20"), 0.0), 60.0)
1646
+ except ValueError:
1647
+ return 20.0
1648
+
1649
+
1650
+ def readiness_poll_seconds() -> float:
1651
+ try:
1652
+ return min(max(float(os.environ.get("PAIRLING_CONNECTD_AUTH_POLL_SECONDS") or "0.5"), 0.1), 2.0)
1653
+ except ValueError:
1654
+ return 0.5
1655
+
1656
+
1657
+ def decision(status: dict):
1658
+ """Return one of: ("open",), ("skip", reason), ("wait",)."""
1659
+ if not status:
1660
+ # connectd not reachable yet — keep waiting until the deadline.
1661
+ return ("wait",)
1662
+ auth_state = str(status.get("auth_state") or "")
1663
+ if auth_state == "authenticated":
1664
+ return ("skip", "already authenticated")
1665
+ tags = status.get("tags")
1666
+ if isinstance(tags, list) and tags:
1667
+ # Already-tagged connectd (machine identity established) — no browser step.
1668
+ return ("skip", "already tagged")
1669
+ if status.get("auth_url_present"):
1670
+ return ("open",)
1671
+ # Reachable but no interactive auth URL yet — give connectd a moment.
1672
+ return ("wait",)
1673
+
1674
+
1675
+ def post_auth_open() -> bool:
1676
+ request = urllib.request.Request(AUTH_OPEN_URL, data=b"", method="POST")
1677
+ try:
1678
+ with urllib.request.urlopen(request, timeout=5) as response:
1679
+ payload = json.loads(response.read().decode("utf-8"))
1680
+ except urllib.error.HTTPError as exc:
1681
+ # connectd answered but the auth URL is not ready (409) or similar.
1682
+ try:
1683
+ payload = json.loads(exc.read().decode("utf-8"))
1684
+ except Exception:
1685
+ payload = {}
1686
+ return bool(payload.get("opened"))
1687
+ except Exception:
1688
+ return False
1689
+ return bool(payload.get("opened"))
1690
+
1691
+
1692
+ def main() -> None:
1693
+ wait_seconds = readiness_wait_seconds()
1694
+ poll_seconds = readiness_poll_seconds()
1695
+ deadline = time.monotonic() + wait_seconds
1696
+ while True:
1697
+ status = fetch_connectd_status(timeout_seconds=0.7)
1698
+ action = decision(status)
1699
+ if action[0] == "open":
1700
+ if post_auth_open():
1701
+ print("Opened the Tailscale sign-in in your browser. Finish sign-in to bring Pairling Connect online; the pairing code follows.")
1702
+ else:
1703
+ print("Pairling Connect sign-in is not ready yet; continuing with the pairing code. You can re-run sign-in later with: pairling connect-auth-open")
1704
+ return
1705
+ if action[0] == "skip":
1706
+ # Already authenticated or tagged — no browser step needed.
1707
+ return
1708
+ if wait_seconds <= 0 or time.monotonic() >= deadline:
1709
+ print("Pairling Connect was not ready in time; continuing with the pairing code. You can sign in later with: pairling connect-auth-open")
1710
+ return
1711
+ time.sleep(min(poll_seconds, max(0.0, deadline - time.monotonic())))
1712
+
1713
+
1714
+ main()
1715
+ PY
1716
+ }
1717
+
1894
1718
  render_pair_qr() {
1895
1719
  local pair_url="$1"
1896
1720
  if ! command -v swift >/dev/null 2>&1; then
@@ -1984,13 +1808,11 @@ commands:
1984
1808
  doctor --first-run --json
1985
1809
  reconcile-ptybroker
1986
1810
  pair
1987
- connect-auth-open
1988
1811
  devices
1989
1812
  unpair <device_id>
1990
1813
  rotate-token <device_id>
1991
1814
  logs
1992
1815
  diagnose --redact
1993
- enable-silent-join [--client-secret PATH] [--yes]
1994
1816
  uninstall
1995
1817
  rollback
1996
1818
  EOF
@@ -2032,9 +1854,6 @@ case "$cmd" in
2032
1854
  pair)
2033
1855
  pair_runtime "$@"
2034
1856
  ;;
2035
- enable-silent-join)
2036
- enable_silent_join "$@"
2037
- ;;
2038
1857
  devices)
2039
1858
  devices_runtime
2040
1859
  ;;
@@ -4,7 +4,6 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
- import os
8
7
  import plistlib
9
8
  from pathlib import Path
10
9
 
@@ -12,7 +11,6 @@ PAIRLING_DAEMON_LABEL = "dev.pairling.companiond"
12
11
  PAIRLING_GUARDIAN_LABEL = "dev.pairling.power-guardian"
13
12
  PAIRLING_CONNECTD_LABEL = "dev.pairling.connectd"
14
13
  PAIRLING_PTYBROKER_LABEL = "dev.pairling.ptybroker"
15
- PAIRLING_MINTD_LABEL = "dev.pairling.mintd"
16
14
  PAIRLING_RUNTIME_PORT = "7773"
17
15
 
18
16
 
@@ -22,7 +20,7 @@ def write_plist(path: Path, payload: dict) -> None:
22
20
  plistlib.dump(payload, fh, sort_keys=False)
23
21
 
24
22
 
25
- def daemon_plist(current: Path, logs: Path, python_bin: str, mint_enabled: bool = False) -> dict:
23
+ def daemon_plist(current: Path, logs: Path, python_bin: str) -> dict:
26
24
  env = {
27
25
  "PAIRLING_RUNTIME_PORT": PAIRLING_RUNTIME_PORT,
28
26
  "COMPANION_DAEMON_PORT": PAIRLING_RUNTIME_PORT,
@@ -31,11 +29,6 @@ def daemon_plist(current: Path, logs: Path, python_bin: str, mint_enabled: bool
31
29
  "PAIRLING_LOGS_ROOT": str(logs),
32
30
  "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
33
31
  }
34
- if mint_enabled:
35
- # Architecture B is on only when the separate-uid mint broker is
36
- # installed. Persisting the flag here keeps it across daemon restarts
37
- # and reboots, instead of relying on an ephemeral launchctl setenv.
38
- env["PAIRLING_MINT_ENABLED"] = "1"
39
32
  return {
40
33
  "Label": PAIRLING_DAEMON_LABEL,
41
34
  "ProgramArguments": [
@@ -104,39 +97,6 @@ def connectd_plist(current: Path, logs: Path) -> dict:
104
97
  }
105
98
 
106
99
 
107
- def mintd_plist(current: Path, logs: Path) -> dict:
108
- system_root = Path("/Library/Application Support/Pairling")
109
- system_logs = Path("/Library/Logs/Pairling")
110
- return {
111
- "Label": PAIRLING_MINTD_LABEL,
112
- "UserName": "_pairling_mint",
113
- "GroupName": "staff",
114
- "ProgramArguments": [
115
- str(system_root / "mint" / "pairling-tailnet-mintd"),
116
- "--secret-path",
117
- str(system_root / "mint" / "client_secret.json"),
118
- "--socket-path",
119
- str(system_root / "run" / "mintd" / "mintd.sock"),
120
- "--state-path",
121
- str(system_root / "mint" / "state.json"),
122
- "--audit-path",
123
- str(system_root / "mint" / "audit.jsonl"),
124
- "--alert-path",
125
- str(system_root / "run" / "mintd" / "alerts.jsonl"),
126
- "--authorized-uid",
127
- str(os.getuid()),
128
- ],
129
- "EnvironmentVariables": {
130
- "PATH": "/Applications/Tailscale.app/Contents/MacOS:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
131
- },
132
- "RunAtLoad": True,
133
- "KeepAlive": True,
134
- "ThrottleInterval": 10,
135
- "StandardOutPath": str(system_logs / "mintd.log"),
136
- "StandardErrorPath": str(system_logs / "mintd.err"),
137
- }
138
-
139
-
140
100
  def ptybroker_plist(current: Path, logs: Path, python_bin: str) -> dict:
141
101
  app_support = current.parent.parent
142
102
  return {
@@ -166,19 +126,16 @@ def main() -> int:
166
126
  parser.add_argument("--daemon-python", default="/usr/local/bin/python3")
167
127
  parser.add_argument("--guardian-python", default="/usr/bin/python3")
168
128
  parser.add_argument("--mirror-python", default="/usr/local/bin/python3", help=argparse.SUPPRESS)
169
- parser.add_argument("--mint-enabled", action="store_true",
170
- help="set PAIRLING_MINT_ENABLED=1 in the companiond env (Architecture B)")
171
129
  args = parser.parse_args()
172
130
 
173
131
  current = Path(args.current_root)
174
132
  logs = Path(args.logs_root)
175
133
  out = Path(args.output_dir)
176
134
 
177
- write_plist(out / f"{PAIRLING_DAEMON_LABEL}.plist", daemon_plist(current, logs, args.daemon_python, args.mint_enabled))
135
+ write_plist(out / f"{PAIRLING_DAEMON_LABEL}.plist", daemon_plist(current, logs, args.daemon_python))
178
136
  write_plist(out / f"{PAIRLING_PTYBROKER_LABEL}.plist", ptybroker_plist(current, logs, args.daemon_python))
179
137
  write_plist(out / f"{PAIRLING_GUARDIAN_LABEL}.plist", guardian_plist(current, logs, args.guardian_python))
180
138
  write_plist(out / f"{PAIRLING_CONNECTD_LABEL}.plist", connectd_plist(current, logs))
181
- write_plist(out / f"{PAIRLING_MINTD_LABEL}.plist", mintd_plist(current, logs))
182
139
  return 0
183
140
 
184
141