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 +10 -11
- package/bin/pairling.mjs +1 -4
- package/package.json +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairlingd.py +1 -143
- package/payload/mac/install/install-runtime.sh +113 -294
- package/payload/mac/install/render-launchd.py +2 -45
- package/payload/mac/install/uninstall-runtime.sh +32 -0
- package/payload-manifest.json +10 -32
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/main.go +0 -121
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd.go +0 -418
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd_test.go +0 -894
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
|
|
31
|
-
|
|
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`
|
|
34
|
-
|
|
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
|
|
55
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
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.
|
|
44
|
-
"@pairling/runtime-darwin-x64": "0.2.
|
|
43
|
+
"@pairling/runtime-darwin-arm64": "0.2.8",
|
|
44
|
+
"@pairling/runtime-darwin-x64": "0.2.8"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
c07a6fd
|
package/payload/mac/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
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": "
|
|
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):
|