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.
package/README.md CHANGED
@@ -27,11 +27,11 @@ Then open Pairling on your iPhone and scan the QR code that `setup` prints.
27
27
  - Stages the runtime under `~/Library/Application Support/Pairling/runtime/`
28
28
  (versioned releases, atomic `current` symlink flip, `pairling rollback`).
29
29
  - Installs user-domain LaunchAgents (`dev.pairling.companiond`,
30
- `dev.pairling.connectd`). No root. The optional power guardian and the
31
- optional silent-join mint broker are separate, explicit, sudo-gated steps.
30
+ `dev.pairling.connectd`). No root. The optional power guardian is a separate,
31
+ explicit, sudo-gated step.
32
32
  - Verifies the payload against the package's integrity manifest and verifies
33
- the Developer ID signature of the bundled `pairling-connectd` and
34
- `pairling-tailnet-mintd` binaries before staging — fail closed.
33
+ the Developer ID signature of the bundled `pairling-connectd` binary before
34
+ staging — fail closed.
35
35
 
36
36
  ## Commands
37
37
 
@@ -51,9 +51,8 @@ pairling uninstall [--yes]
51
51
  appears in a published manifest.
52
52
  - **Provenance:** releases are published via npm Trusted Publishing (OIDC) with
53
53
  provenance attestations. Verify with `npm audit signatures`.
54
- - **Readable payload:** the runtime is Python/bash source plus two signed Go
55
- binaries (`pairling-connectd` and the `pairling-tailnet-mintd` broker);
56
- inspect it with `npm pack pairling --dry-run`.
54
+ - **Readable payload:** the runtime is Python/bash source plus one signed Go
55
+ binary (`pairling-connectd`); inspect it with `npm pack pairling --dry-run`.
57
56
  - **Integrity chain:** CI records SHA-256 of every payload file in
58
57
  `payload-manifest.json`; `pairling setup` re-verifies before staging;
59
58
  `pairling doctor` re-verifies the staged runtime and the binary signature.
@@ -62,10 +61,10 @@ pairling uninstall [--yes]
62
61
 
63
62
  ## Platform packages
64
63
 
65
- The compiled runtime binaries (`pairling-connectd` and the optional
66
- `pairling-tailnet-mintd` silent-join broker) ship as platform-filtered optional
67
- dependencies: `@pairling/runtime-darwin-arm64` and `@pairling/runtime-darwin-x64`
68
- — signed, notarized, and hash-pinned by each package's integrity manifest.
64
+ The compiled runtime binary (`pairling-connectd`) ships via platform-filtered
65
+ optional dependencies: `@pairling/runtime-darwin-arm64` and
66
+ `@pairling/runtime-darwin-x64` — signed, notarized, and hash-pinned by each
67
+ package's integrity manifest.
69
68
 
70
69
  ## Links
71
70
 
package/bin/pairling.mjs CHANGED
@@ -92,7 +92,6 @@ function detectRosetta() {
92
92
  function shimEnv() {
93
93
  const runtimeDir = runtimePackageDir();
94
94
  const connectd = runtimeDir ? join(runtimeDir, "bin", "pairling-connectd") : null;
95
- const mintd = runtimeDir ? join(runtimeDir, "bin", "pairling-tailnet-mintd") : null;
96
95
  const vendoredPython = runtimeDir ? join(runtimeDir, "python", "bin", "python3") : null;
97
96
  return {
98
97
  packageRoot,
@@ -101,7 +100,6 @@ function shimEnv() {
101
100
  payloadRoot,
102
101
  runtimePackageDir: runtimeDir,
103
102
  connectdPath: connectd && existsSync(connectd) ? connectd : null,
104
- mintdPath: mintd && existsSync(mintd) ? mintd : null,
105
103
  vendoredPython: vendoredPython && existsSync(vendoredPython) ? vendoredPython : null,
106
104
  stagedCli: stagedCliPath(),
107
105
  stagedRuntimeVersion: stagedRuntimeVersion(),
@@ -189,7 +187,7 @@ function main() {
189
187
 
190
188
  if (existsSync(payloadCli)) {
191
189
  const env = shimEnv();
192
- if (!env.runtimePackageDir || !env.connectdPath || !env.mintdPath) {
190
+ if (!env.runtimePackageDir || !env.connectdPath) {
193
191
  process.stderr.write(
194
192
  [
195
193
  "pairling: the platform runtime package is missing or incomplete.",
@@ -208,7 +206,6 @@ function main() {
208
206
  delegate(payloadCli, args, {
209
207
  PAIRLING_REPO_ROOT: join(payloadRoot, "."),
210
208
  PAIRLING_CONNECTD_PREBUILT: env.connectdPath,
211
- PAIRLING_MINTD_PREBUILT: env.mintdPath,
212
209
  PAIRLING_DAEMON_PYTHON: env.vendoredPython,
213
210
  });
214
211
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairling",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Pair your iPhone with the AI coding agents running on your Mac. CLI and local runtime installer for the Pairling iOS app.",
5
5
  "keywords": [
6
6
  "pairling",
@@ -40,7 +40,7 @@
40
40
  "url": "https://github.com/mergimg0/pairling-helper"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@pairling/runtime-darwin-arm64": "0.2.7",
44
- "@pairling/runtime-darwin-x64": "0.2.7"
43
+ "@pairling/runtime-darwin-arm64": "0.2.8",
44
+ "@pairling/runtime-darwin-x64": "0.2.8"
45
45
  }
46
46
  }
@@ -1 +1 @@
1
- 102b7cf
1
+ c07a6fd
@@ -1 +1 @@
1
- 0.2.7
1
+ 0.2.8
@@ -300,8 +300,6 @@ os.environ["PATH"] = (
300
300
 
301
301
  PORT = RUNTIME_PORT
302
302
  HOME = Path.home()
303
- MINT_SOCKET_PATH = "/Library/Application Support/Pairling/run/mintd/mintd.sock"
304
- MINT_ALERT_PATH = Path("/Library/Application Support/Pairling/run/mintd/alerts.jsonl")
305
303
  DEFAULT_COORDINATOR_HOST = (
306
304
  os.environ.get("PAIRLING_HOSTNAME")
307
305
  or os.environ.get("COMPANION_COORDINATOR_HOST")
@@ -2141,81 +2139,6 @@ def _lan_ips(power_state: dict | None = None) -> list[str]:
2141
2139
  return _cached_probe("lan_ips", _HEALTH_PROBE_CACHE_SECONDS, _probe_lan_ips)
2142
2140
 
2143
2141
 
2144
- def _mint_phone_authkey(pair_id: str) -> dict | None:
2145
- if not _pairling_mint_enabled():
2146
- return None
2147
- path = os.environ.get("PAIRLING_MINT_SOCKET") or MINT_SOCKET_PATH
2148
- try:
2149
- with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
2150
- sock.settimeout(float(os.environ.get("PAIRLING_MINT_TIMEOUT_SECONDS", "0.8")))
2151
- sock.connect(path)
2152
- sock.sendall(json.dumps({"op": "mint_phone_key", "pair_id": pair_id}).encode("utf-8") + b"\n")
2153
- with sock.makefile("rb") as fh:
2154
- raw = fh.readline(8192)
2155
- payload = json.loads(raw.decode("utf-8"))
2156
- except Exception:
2157
- return None
2158
- if not isinstance(payload, dict) or not payload.get("ok"):
2159
- return None
2160
- key = str(payload.get("authkey") or "")
2161
- if not key:
2162
- return None
2163
- try:
2164
- expires_at = int(payload.get("expires_at") or 0)
2165
- except (TypeError, ValueError):
2166
- expires_at = 0
2167
- return {
2168
- "authkey": key,
2169
- "key_id": payload.get("key_id"),
2170
- "expires_at": expires_at,
2171
- }
2172
-
2173
-
2174
- def _pairling_mint_enabled() -> bool:
2175
- return os.environ.get("PAIRLING_MINT_ENABLED", "").strip().lower() in {"1", "true", "yes"}
2176
-
2177
-
2178
- def _silent_join_capability(mint_enabled: bool, connectd_status: dict | None) -> tuple[bool, str]:
2179
- """D1: whether a cellular/funnel phone can complete a silent join. Minting must
2180
- be enabled, and the tailnet must not be under network lock (mintd refuses to
2181
- mint under lock, mintd.go:165-171). Pure, so the phone is told up front."""
2182
- if not mint_enabled:
2183
- return False, "mint_disabled"
2184
- if connectd_status and connectd_status.get("tailnet_lock_enabled"):
2185
- return False, "tailnet_lock"
2186
- return True, ""
2187
-
2188
-
2189
- def _mintd_alert_snapshot(limit: int = 5) -> list[dict]:
2190
- path = Path(os.environ.get("PAIRLING_MINT_ALERT_PATH") or MINT_ALERT_PATH)
2191
- try:
2192
- lines = path.read_text().splitlines()
2193
- except Exception:
2194
- return []
2195
- allowed = {"event", "ts", "pair_id", "key_id", "uid", "window"}
2196
- alerts: list[dict] = []
2197
- for line in lines[-max(1, limit):]:
2198
- try:
2199
- item = json.loads(line)
2200
- except Exception:
2201
- continue
2202
- if not isinstance(item, dict):
2203
- continue
2204
- sanitized = {key: item[key] for key in allowed if key in item}
2205
- if sanitized:
2206
- alerts.append(sanitized)
2207
- return alerts
2208
-
2209
-
2210
- def _attach_mintd_alerts(coordinator: dict) -> dict:
2211
- alerts = _mintd_alert_snapshot()
2212
- if not alerts:
2213
- return coordinator
2214
- out = dict(coordinator)
2215
- out["mintd_alerts"] = alerts
2216
- return out
2217
-
2218
-
2219
2142
  def _guardian_listener_entries(power_state: dict | None) -> list[str]:
2220
2143
  if not isinstance(power_state, dict):
2221
2144
  return []
@@ -2592,7 +2515,6 @@ def _health_payload(full_power: bool = False, authenticated: bool = False, auth_
2592
2515
  if connect.get("summary") is not None:
2593
2516
  coordinator = dict(coordinator)
2594
2517
  coordinator["pairling_connect"] = connect["summary"]
2595
- coordinator = _attach_mintd_alerts(coordinator)
2596
2518
  routes = _health_routes(coordinator, power_state)
2597
2519
  ok = coordinator.get("posture") in ("ready", "warning")
2598
2520
  runtime_info = _runtime_info_snapshot()
@@ -2658,7 +2580,6 @@ def _mac_health_alert_snapshot() -> dict:
2658
2580
  coordinator = _apply_pairling_connect_posture(
2659
2581
  coordinator, _normalize_guardian_state(state), _pairling_connect_health()
2660
2582
  )
2661
- coordinator = _attach_mintd_alerts(coordinator)
2662
2583
  return {
2663
2584
  "ok": coordinator.get("posture") in ("ready", "warning"),
2664
2585
  "schema_version": 1,
@@ -8236,15 +8157,6 @@ class Handler(BaseHTTPRequestHandler):
8236
8157
  else {"ok": False, "reason": "advertiser_unavailable"}
8237
8158
  )
8238
8159
  pairling_connect_routes = self._pairling_connect_routes()
8239
- connectd_status = {}
8240
- if fetch_connectd_status is not None:
8241
- try:
8242
- connectd_status = fetch_connectd_status(timeout_seconds=0.7) or {}
8243
- except Exception:
8244
- connectd_status = {}
8245
- silent_join_available, silent_join_reason = _silent_join_capability(
8246
- _pairling_mint_enabled(), connectd_status
8247
- )
8248
8160
  except ValueError as exc:
8249
8161
  self._send_json({"ok": False, "error": {"code": "bad_request", "message": str(exc)}}, status=400)
8250
8162
  return
@@ -8271,9 +8183,7 @@ class Handler(BaseHTTPRequestHandler):
8271
8183
  "pairing_nonce": started.pairing_nonce,
8272
8184
  "attest_challenge": started.attest_challenge,
8273
8185
  "mac_ake_pub": started.mac_ake_pub,
8274
- "pv": "3" if started.mac_ake_pub and _pairling_mint_enabled() else "2" if started.mac_ake_pub else "1",
8275
- "silent_join_available": silent_join_available,
8276
- "silent_join_unavailable_reason": silent_join_reason,
8186
+ "pv": "2" if started.mac_ake_pub else "1",
8277
8187
  },
8278
8188
  })
8279
8189
 
@@ -8393,55 +8303,6 @@ class Handler(BaseHTTPRequestHandler):
8393
8303
  proof_secret_nonce, enc_proof_secret = PAIRING_STORE.seal_psk_token(
8394
8304
  k_token, claim.device.proof_secret, proof_secret_aad
8395
8305
  )
8396
- try:
8397
- protocol_version = int(payload.get("pv") or 0)
8398
- except (TypeError, ValueError):
8399
- protocol_version = 0
8400
- # Increment 5: validate pv to the known set and gate the mint. A
8401
- # funnel-origin claim mints only with a verified App Attest, so a
8402
- # script holding a stolen secret cannot obtain a tagged auth key.
8403
- if protocol_version not in (1, 2, 3):
8404
- protocol_version = 0
8405
- mint_allowed = protocol_version >= 3 and (
8406
- not funnel_origin or claim.direct_attestation_verified
8407
- )
8408
- tailnet_authkey = _mint_phone_authkey(str(payload.get("pair_id") or "")) if mint_allowed else None
8409
- authkey_nonce = None
8410
- enc_authkey = None
8411
- if tailnet_authkey:
8412
- authkey_aad = aad + b"\npairling.psk.enc_authkey.v1"
8413
- authkey_nonce, enc_authkey = PAIRING_STORE.seal_psk_token(
8414
- k_token,
8415
- str(tailnet_authkey.get("authkey") or ""),
8416
- authkey_aad,
8417
- )
8418
- if funnel_origin:
8419
- # Operator visibility: a remote, funnel-origin join has no
8420
- # proximity backstop, so make it loud and auditable, and push a
8421
- # revoke prompt to the already-paired devices (D4).
8422
- try:
8423
- import sys as _sys
8424
- _sys.stderr.write(
8425
- "PAIRLING FUNNEL JOIN: a remote device paired over Funnel "
8426
- f"(device_id={claim.device.device_id}); no proximity backstop, "
8427
- "review and revoke if unexpected.\n"
8428
- )
8429
- _sys.stderr.flush()
8430
- except Exception:
8431
- pass
8432
- if PUSH_DISPATCHER is not None:
8433
- try:
8434
- PUSH_DISPATCHER.broadcast_alert(
8435
- exclude_device_id=claim.device.device_id,
8436
- event_id=f"funnel_join:{claim.device.device_id}",
8437
- kind="remote_join",
8438
- route="pairling-connect",
8439
- title="New device paired remotely",
8440
- body="A device joined your Mac over the internet. Tap to review or revoke.",
8441
- pairling_extra={"device_id": claim.device.device_id, "revoke_path": "/pair/revoke"},
8442
- )
8443
- except Exception:
8444
- pass
8445
8306
  if PAIRING_ADVERTISER is not None:
8446
8307
  PAIRING_ADVERTISER.stop()
8447
8308
  except PairingError as exc:
@@ -8481,9 +8342,6 @@ class Handler(BaseHTTPRequestHandler):
8481
8342
  "routes": runtime_routes,
8482
8343
  },
8483
8344
  }
8484
- if authkey_nonce and enc_authkey:
8485
- response["enc_authkey"] = base64.b64encode(enc_authkey).decode("ascii")
8486
- response["authkey_nonce"] = base64.b64encode(authkey_nonce).decode("ascii")
8487
8345
  self._send_json(response)
8488
8346
 
8489
8347
  def _handle_pair_reauth_challenge(self, q):