pairling 0.0.1 → 0.1.0
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/package.json +5 -1
- package/payload/mac/SOURCE_BRANCH +1 -0
- package/payload/mac/SOURCE_DIRTY +1 -0
- package/payload/mac/SOURCE_REVISION +1 -0
- package/payload/mac/VERSION +1 -0
- package/payload/mac/companiond/integrations/__init__.py +1 -0
- package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
- package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
- package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
- package/payload/mac/companiond/live_activity_publisher.py +380 -0
- package/payload/mac/companiond/llm_route.py +108 -0
- package/payload/mac/companiond/local_mcp_bridge.py +156 -0
- package/payload/mac/companiond/model_status_contract.py +101 -0
- package/payload/mac/companiond/pairdrop_store.py +920 -0
- package/payload/mac/companiond/pairling_connectd_status.py +149 -0
- package/payload/mac/companiond/pairling_devices.py +459 -0
- package/payload/mac/companiond/pairling_pairing.py +404 -0
- package/payload/mac/companiond/pairling_relay_claims.py +232 -0
- package/payload/mac/companiond/pairling_tools.py +706 -0
- package/payload/mac/companiond/pairlingd.py +18438 -0
- package/payload/mac/companiond/providers/__init__.py +1 -0
- package/payload/mac/companiond/providers/base.py +255 -0
- package/payload/mac/companiond/providers/claude.py +127 -0
- package/payload/mac/companiond/providers/codex.py +124 -0
- package/payload/mac/companiond/providers/external.py +46 -0
- package/payload/mac/companiond/providers/registry.py +70 -0
- package/payload/mac/companiond/pty_broker.py +887 -0
- package/payload/mac/companiond/push_dispatcher.py +1990 -0
- package/payload/mac/companiond/push_event_catalog.py +566 -0
- package/payload/mac/companiond/request_proof.py +142 -0
- package/payload/mac/companiond/runtime_contract.py +47 -0
- package/payload/mac/companiond/runtime_manifest.py +197 -0
- package/payload/mac/companiond/runtime_paths.py +87 -0
- package/payload/mac/companiond/safety_monitor.py +542 -0
- package/payload/mac/companiond/sentinel_notifications.py +491 -0
- package/payload/mac/companiond/standard_push_publisher.py +516 -0
- package/payload/mac/companiond/substrate_status_contract.py +139 -0
- package/payload/mac/companiond/terminal_screen_backend.py +332 -0
- package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
- package/payload/mac/companiond/workstate_feed_contract.py +108 -0
- package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
- package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
- package/payload/mac/connectd/go.mod +51 -0
- package/payload/mac/connectd/go.sum +229 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
- package/payload/mac/connectd/internal/runtime/config.go +99 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
- package/payload/mac/connectd/internal/status/status.go +300 -0
- package/payload/mac/connectd/internal/status/status_test.go +263 -0
- package/payload/mac/guardian/companion-power-guardian.py +613 -0
- package/payload/mac/guardian/guardian_contract.py +67 -0
- package/payload/mac/install/bootstrap-first-run.sh +206 -0
- package/payload/mac/install/doctor.sh +660 -0
- package/payload/mac/install/install-runtime.sh +1241 -0
- package/payload/mac/install/render-launchd.py +119 -0
- package/payload/mac/install/uninstall-runtime.sh +136 -0
- package/payload/mac/mcp/phone_tools.py +210 -0
- package/payload/mac/packaging/bin/pairling +63 -0
- package/payload-manifest.json +255 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Request-bound HMAC proof verification for mutating Pairling endpoints."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
INSTALL_ID_HEADER = "Pairling-Install-ID"
|
|
14
|
+
REQUEST_ID_HEADER = "Pairling-Request-ID"
|
|
15
|
+
TIMESTAMP_HEADER = "Pairling-Timestamp"
|
|
16
|
+
BODY_SHA256_HEADER = "Pairling-Body-SHA256"
|
|
17
|
+
PROOF_HEADER = "Pairling-Proof"
|
|
18
|
+
SKEW_MS = 10 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ProofVerificationResult:
|
|
23
|
+
ok: bool
|
|
24
|
+
status: int = 200
|
|
25
|
+
code: str = "ok"
|
|
26
|
+
message: str = "ok"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReplayCache:
|
|
30
|
+
def __init__(self, *, retention_seconds: int = 600, max_entries: int = 4096):
|
|
31
|
+
self.retention_seconds = retention_seconds
|
|
32
|
+
self.max_entries = max_entries
|
|
33
|
+
self._seen: dict[tuple[str, str], float] = {}
|
|
34
|
+
|
|
35
|
+
def check_and_store(self, *, device_id: str, request_id: str, now: float | None = None) -> bool:
|
|
36
|
+
current = time.time() if now is None else now
|
|
37
|
+
cutoff = current - self.retention_seconds
|
|
38
|
+
if len(self._seen) > self.max_entries:
|
|
39
|
+
self._seen = {
|
|
40
|
+
key: ts for key, ts in self._seen.items()
|
|
41
|
+
if ts >= cutoff
|
|
42
|
+
}
|
|
43
|
+
key = (device_id, request_id)
|
|
44
|
+
if key in self._seen and self._seen[key] >= cutoff:
|
|
45
|
+
return False
|
|
46
|
+
self._seen[key] = current
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def body_sha256_hex(body: bytes) -> str:
|
|
51
|
+
return hashlib.sha256(body).hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def canonical_request(
|
|
55
|
+
*,
|
|
56
|
+
method: str,
|
|
57
|
+
path_and_query: str,
|
|
58
|
+
timestamp_ms: str,
|
|
59
|
+
request_id: str,
|
|
60
|
+
body_sha256: str,
|
|
61
|
+
install_id: str,
|
|
62
|
+
device_id: str,
|
|
63
|
+
) -> str:
|
|
64
|
+
return "\n".join([
|
|
65
|
+
method.upper(),
|
|
66
|
+
path_and_query,
|
|
67
|
+
timestamp_ms,
|
|
68
|
+
request_id,
|
|
69
|
+
body_sha256,
|
|
70
|
+
install_id,
|
|
71
|
+
device_id,
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def proof_hex(*, secret: str, canonical: str) -> str:
|
|
76
|
+
return hmac.new(
|
|
77
|
+
secret.encode("utf-8"),
|
|
78
|
+
canonical.encode("utf-8"),
|
|
79
|
+
hashlib.sha256,
|
|
80
|
+
).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def verify_request_proof(
|
|
84
|
+
*,
|
|
85
|
+
headers: Any,
|
|
86
|
+
method: str,
|
|
87
|
+
path_and_query: str,
|
|
88
|
+
body: bytes,
|
|
89
|
+
auth_result: Any,
|
|
90
|
+
local_install_id: str,
|
|
91
|
+
replay_cache: ReplayCache,
|
|
92
|
+
now_ms: int | None = None,
|
|
93
|
+
) -> ProofVerificationResult:
|
|
94
|
+
proof_secret = str(getattr(auth_result, "proof_secret", "") or "").strip()
|
|
95
|
+
if not proof_secret:
|
|
96
|
+
return ProofVerificationResult(False, 403, "missing_proof_secret", "Pair this Mac again to enable request proof.")
|
|
97
|
+
|
|
98
|
+
install_id = _header(headers, INSTALL_ID_HEADER)
|
|
99
|
+
request_id = _header(headers, REQUEST_ID_HEADER)
|
|
100
|
+
timestamp_ms = _header(headers, TIMESTAMP_HEADER)
|
|
101
|
+
body_hash = _header(headers, BODY_SHA256_HEADER)
|
|
102
|
+
proof = _header(headers, PROOF_HEADER)
|
|
103
|
+
|
|
104
|
+
if not install_id or not request_id or not timestamp_ms or not body_hash or not proof:
|
|
105
|
+
return ProofVerificationResult(False, 401, "missing_proof", "Request proof headers are required.")
|
|
106
|
+
if install_id != local_install_id:
|
|
107
|
+
return ProofVerificationResult(False, 403, "install_id_mismatch", "Request proof was for a different Mac.")
|
|
108
|
+
try:
|
|
109
|
+
parsed_ts = int(timestamp_ms)
|
|
110
|
+
except ValueError:
|
|
111
|
+
return ProofVerificationResult(False, 401, "bad_timestamp", "Request proof timestamp is invalid.")
|
|
112
|
+
current_ms = int(time.time() * 1000) if now_ms is None else now_ms
|
|
113
|
+
if abs(current_ms - parsed_ts) > SKEW_MS:
|
|
114
|
+
return ProofVerificationResult(False, 401, "stale_timestamp", "Request proof timestamp is stale.")
|
|
115
|
+
|
|
116
|
+
expected_body_hash = body_sha256_hex(body)
|
|
117
|
+
if not hmac.compare_digest(body_hash.lower(), expected_body_hash):
|
|
118
|
+
return ProofVerificationResult(False, 401, "body_hash_mismatch", "Request body hash does not match.")
|
|
119
|
+
|
|
120
|
+
device_id = str(getattr(auth_result, "device_id", "") or "")
|
|
121
|
+
canonical = canonical_request(
|
|
122
|
+
method=method,
|
|
123
|
+
path_and_query=path_and_query,
|
|
124
|
+
timestamp_ms=timestamp_ms,
|
|
125
|
+
request_id=request_id,
|
|
126
|
+
body_sha256=expected_body_hash,
|
|
127
|
+
install_id=install_id,
|
|
128
|
+
device_id=device_id,
|
|
129
|
+
)
|
|
130
|
+
expected_proof = proof_hex(secret=proof_secret, canonical=canonical)
|
|
131
|
+
if not hmac.compare_digest(proof.lower(), expected_proof):
|
|
132
|
+
return ProofVerificationResult(False, 401, "bad_proof", "Request proof did not verify.")
|
|
133
|
+
if not replay_cache.check_and_store(device_id=device_id, request_id=request_id, now=current_ms / 1000):
|
|
134
|
+
return ProofVerificationResult(False, 409, "replayed_request", "Request proof was already used.")
|
|
135
|
+
return ProofVerificationResult(True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _header(headers: Any, name: str) -> str:
|
|
139
|
+
try:
|
|
140
|
+
return str(headers.get(name, "") or "").strip()
|
|
141
|
+
except AttributeError:
|
|
142
|
+
return str((headers or {}).get(name, "") or "").strip()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared constants for the Pairling Mac runtime."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
SCHEMA_VERSION = 1
|
|
9
|
+
POWER_STATE_SCHEMA_VERSION = 1
|
|
10
|
+
RUNTIME_NAME = "pairling-mac-runtime"
|
|
11
|
+
CONTRACT_VERSION = "pairling-runtime-v1"
|
|
12
|
+
PAIRLING_CONTRACT_VERSION = CONTRACT_VERSION
|
|
13
|
+
COMPAT_MODE = "pairling-v1"
|
|
14
|
+
PORT = int(os.environ.get("PAIRLING_RUNTIME_PORT", "7773"))
|
|
15
|
+
LEGACY_PORT = 7723
|
|
16
|
+
DAEMON_LABEL = "dev.pairling.companiond"
|
|
17
|
+
GUARDIAN_LABEL = "dev.pairling.power-guardian"
|
|
18
|
+
LEGACY_DAEMON_LABEL = "com.mghome.notify-webhook"
|
|
19
|
+
LEGACY_GUARDIAN_LABEL = "com.mghome.companion-power-guardian"
|
|
20
|
+
LEGACY_TOKEN_RELATIVE_PATH = ".claude/scripts/.notify-token"
|
|
21
|
+
POWER_STATE_PATH = "/var/run/pairling-power-state.json"
|
|
22
|
+
TAILSCALE_VARIANT = "standalone"
|
|
23
|
+
AUTH_MODE = "scoped-device-bearer"
|
|
24
|
+
PAIR_SERVICE_TYPE = "_pairling-pair._tcp"
|
|
25
|
+
RUNTIME_BONJOUR_ADVERTISED = False
|
|
26
|
+
|
|
27
|
+
DEFAULT_DEVICE_SCOPES = frozenset({
|
|
28
|
+
"health:read",
|
|
29
|
+
"manifest:read",
|
|
30
|
+
"sessions:read",
|
|
31
|
+
"transcript:read",
|
|
32
|
+
"session:send",
|
|
33
|
+
"session:spawn",
|
|
34
|
+
"session:signal",
|
|
35
|
+
"worker:read",
|
|
36
|
+
"worker:control",
|
|
37
|
+
"llm:route",
|
|
38
|
+
"pairling-tools:run",
|
|
39
|
+
"files:upload",
|
|
40
|
+
"files:read",
|
|
41
|
+
"files:write",
|
|
42
|
+
"files:delete",
|
|
43
|
+
"pair:admin",
|
|
44
|
+
"phone-tools:reverse",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
SUPPORTED_CONTRACTS = {CONTRACT_VERSION}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Runtime manifest loading and verification for the Mac companion daemon."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from runtime_contract import (
|
|
14
|
+
AUTH_MODE,
|
|
15
|
+
COMPAT_MODE,
|
|
16
|
+
CONTRACT_VERSION,
|
|
17
|
+
DAEMON_LABEL,
|
|
18
|
+
PAIR_SERVICE_TYPE,
|
|
19
|
+
PORT,
|
|
20
|
+
RUNTIME_BONJOUR_ADVERTISED,
|
|
21
|
+
RUNTIME_NAME,
|
|
22
|
+
TAILSCALE_VARIANT,
|
|
23
|
+
)
|
|
24
|
+
from runtime_paths import release_root_for
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sha256_file(path: Path) -> str:
|
|
28
|
+
digest = hashlib.sha256()
|
|
29
|
+
with path.open("rb") as fh:
|
|
30
|
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
31
|
+
digest.update(chunk)
|
|
32
|
+
return digest.hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def utc_now_iso() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_manifest_for(script_path: str | Path) -> tuple[dict[str, Any] | None, Path | None, str | None]:
|
|
40
|
+
root = release_root_for(script_path)
|
|
41
|
+
if root is None:
|
|
42
|
+
return None, None, "manifest not found for script path"
|
|
43
|
+
path = root / "manifest.json"
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(path.read_text())
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
return None, path, f"{type(exc).__name__}: {exc}"
|
|
48
|
+
if not isinstance(data, dict):
|
|
49
|
+
return None, path, "manifest root is not an object"
|
|
50
|
+
return data, path, None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _manifest_file_hash(manifest: dict[str, Any], relative_path: str) -> str | None:
|
|
54
|
+
for item in manifest.get("files") or []:
|
|
55
|
+
if isinstance(item, dict) and item.get("path") == relative_path:
|
|
56
|
+
value = item.get("sha256")
|
|
57
|
+
return value if isinstance(value, str) else None
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_runtime_info(
|
|
62
|
+
script_path: str | Path,
|
|
63
|
+
*,
|
|
64
|
+
relative_path: str = "companiond/pairlingd.py",
|
|
65
|
+
launchd_label: str = DAEMON_LABEL,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
script = Path(script_path).resolve()
|
|
68
|
+
manifest, manifest_path, manifest_error = load_manifest_for(script)
|
|
69
|
+
runtime_version = os.environ.get("COMPANION_RUNTIME_VERSION", "legacy")
|
|
70
|
+
source_revision = os.environ.get("COMPANION_SOURCE_REVISION", "unknown")
|
|
71
|
+
source_branch = os.environ.get("COMPANION_SOURCE_BRANCH", "unknown")
|
|
72
|
+
source_dirty = None
|
|
73
|
+
installed_at = os.environ.get("COMPANION_INSTALLED_AT")
|
|
74
|
+
install_root = str(script.parent.parent) if script.parent.name in {"companiond", "guardian"} else str(script.parent)
|
|
75
|
+
source_hash = None
|
|
76
|
+
verified = False
|
|
77
|
+
verification_error = manifest_error
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
source_hash = sha256_file(script)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
verification_error = f"{type(exc).__name__}: {exc}"
|
|
83
|
+
|
|
84
|
+
if manifest:
|
|
85
|
+
runtime_version = str(manifest.get("runtime_version") or runtime_version)
|
|
86
|
+
source_revision = str(manifest.get("source_revision") or source_revision)
|
|
87
|
+
source_branch = str(manifest.get("source_branch") or source_branch)
|
|
88
|
+
if "source_dirty" in manifest:
|
|
89
|
+
source_dirty = bool(manifest.get("source_dirty"))
|
|
90
|
+
installed_at = str(manifest.get("installed_at") or installed_at or "")
|
|
91
|
+
install_root = str(manifest.get("install_root") or install_root)
|
|
92
|
+
expected_hash = _manifest_file_hash(manifest, relative_path)
|
|
93
|
+
if expected_hash and source_hash:
|
|
94
|
+
verified = expected_hash == source_hash
|
|
95
|
+
if not verified:
|
|
96
|
+
verification_error = f"hash mismatch for {relative_path}"
|
|
97
|
+
else:
|
|
98
|
+
verification_error = f"manifest missing hash for {relative_path}"
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"name": RUNTIME_NAME,
|
|
102
|
+
"runtime_version": runtime_version,
|
|
103
|
+
"contract_version": CONTRACT_VERSION,
|
|
104
|
+
"source_revision": source_revision,
|
|
105
|
+
"source_branch": source_branch,
|
|
106
|
+
"source_dirty": source_dirty,
|
|
107
|
+
"installed_at": installed_at or None,
|
|
108
|
+
"install_root": install_root,
|
|
109
|
+
"compat_mode": COMPAT_MODE,
|
|
110
|
+
"launchd_label": launchd_label,
|
|
111
|
+
"port": PORT,
|
|
112
|
+
"tailscale_variant": TAILSCALE_VARIANT,
|
|
113
|
+
"verified": verified,
|
|
114
|
+
"source_hash": source_hash,
|
|
115
|
+
"manifest_path": str(manifest_path) if manifest_path else None,
|
|
116
|
+
"manifest_error": verification_error,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def public_runtime_info(info: dict[str, Any]) -> dict[str, Any]:
|
|
121
|
+
"""Return the unauthenticated-safe subset of runtime metadata."""
|
|
122
|
+
return {
|
|
123
|
+
"name": info.get("name") or RUNTIME_NAME,
|
|
124
|
+
"runtime_version": info.get("runtime_version"),
|
|
125
|
+
"contract_version": info.get("contract_version") or CONTRACT_VERSION,
|
|
126
|
+
"compat_mode": info.get("compat_mode") or COMPAT_MODE,
|
|
127
|
+
"launchd_label": info.get("launchd_label") or DAEMON_LABEL,
|
|
128
|
+
"port": info.get("port") or PORT,
|
|
129
|
+
"tailscale_variant": info.get("tailscale_variant") or TAILSCALE_VARIANT,
|
|
130
|
+
"verified": bool(info.get("verified")),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_manifest_payload(
|
|
135
|
+
runtime_info: dict[str, Any],
|
|
136
|
+
*,
|
|
137
|
+
authenticated: bool,
|
|
138
|
+
device_id: str | None = None,
|
|
139
|
+
scopes: list[str] | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
payload: dict[str, Any] = {
|
|
142
|
+
"ok": True,
|
|
143
|
+
"schema_version": 1,
|
|
144
|
+
"contract_version": CONTRACT_VERSION,
|
|
145
|
+
"runtime": public_runtime_info(runtime_info),
|
|
146
|
+
"auth": {
|
|
147
|
+
"mode": AUTH_MODE,
|
|
148
|
+
"required": True,
|
|
149
|
+
"legacy_global_token": False,
|
|
150
|
+
"authenticated": authenticated,
|
|
151
|
+
},
|
|
152
|
+
"network": {
|
|
153
|
+
"runtime_port": PORT,
|
|
154
|
+
"pair_service_type": PAIR_SERVICE_TYPE,
|
|
155
|
+
"runtime_bonjour_advertised": RUNTIME_BONJOUR_ADVERTISED,
|
|
156
|
+
"route_diagnostics": {
|
|
157
|
+
"bonjour": {
|
|
158
|
+
"service_type": PAIR_SERVICE_TYPE,
|
|
159
|
+
"runtime_port": PORT,
|
|
160
|
+
"txt_version": "2",
|
|
161
|
+
},
|
|
162
|
+
"tailnet": {
|
|
163
|
+
"variant": TAILSCALE_VARIANT,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"endpoints": {
|
|
168
|
+
"public": ["/health", "/manifest", "/pair/start", "/pair/claim"],
|
|
169
|
+
"authenticated": [
|
|
170
|
+
"/manifest",
|
|
171
|
+
"/sessions",
|
|
172
|
+
"/sessions-stream",
|
|
173
|
+
"/session-live-events",
|
|
174
|
+
"/transcript",
|
|
175
|
+
"/transcript-stream",
|
|
176
|
+
"/send-text",
|
|
177
|
+
"/inject-now",
|
|
178
|
+
"/worker-kill",
|
|
179
|
+
"/pairling-tools/run",
|
|
180
|
+
"/phone-tools/availability",
|
|
181
|
+
"/phone-tools/next",
|
|
182
|
+
"/phone-tools/result",
|
|
183
|
+
"/sentinel/status",
|
|
184
|
+
"/sentinel/preferences",
|
|
185
|
+
"/sentinel/snooze",
|
|
186
|
+
"/sentinel/evaluate-now",
|
|
187
|
+
"/sentinel/events",
|
|
188
|
+
"/pair/revoke",
|
|
189
|
+
"/pair/rotate-token",
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
if authenticated:
|
|
194
|
+
payload["runtime"] = runtime_info
|
|
195
|
+
payload["auth"]["device_id"] = device_id
|
|
196
|
+
payload["auth"]["scopes"] = sorted(scopes or [])
|
|
197
|
+
return payload
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Path resolver for the Pairling runtime."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from runtime_contract import LEGACY_TOKEN_RELATIVE_PATH, POWER_STATE_PATH
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def home() -> Path:
|
|
13
|
+
return Path.home()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def app_support_root() -> Path:
|
|
17
|
+
return Path(
|
|
18
|
+
os.environ.get(
|
|
19
|
+
"PAIRLING_APP_SUPPORT_ROOT",
|
|
20
|
+
os.environ.get(
|
|
21
|
+
"COMPANION_APP_SUPPORT_ROOT",
|
|
22
|
+
str(home() / "Library" / "Application Support" / "Pairling"),
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def logs_root() -> Path:
|
|
29
|
+
return Path(
|
|
30
|
+
os.environ.get(
|
|
31
|
+
"PAIRLING_LOGS_ROOT",
|
|
32
|
+
os.environ.get(
|
|
33
|
+
"COMPANION_LOGS_ROOT",
|
|
34
|
+
str(home() / "Library" / "Logs" / "Pairling"),
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def runtime_root() -> Path:
|
|
41
|
+
return app_support_root() / "runtime"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def current_release() -> Path:
|
|
45
|
+
return runtime_root() / "current"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def state_root() -> Path:
|
|
49
|
+
return app_support_root() / "state"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def install_history_path() -> Path:
|
|
53
|
+
return state_root() / "install-history.jsonl"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def install_id_path() -> Path:
|
|
57
|
+
return state_root() / "install-id"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def devices_db_path() -> Path:
|
|
61
|
+
return app_support_root() / "devices.sqlite"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def audit_log_path() -> Path:
|
|
65
|
+
return logs_root() / "audit.jsonl"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def token_path() -> Path:
|
|
69
|
+
return Path(os.environ.get("NOTIFY_TOKEN_FILE", str(home() / LEGACY_TOKEN_RELATIVE_PATH)))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def guardian_state_path() -> Path:
|
|
73
|
+
return Path(os.environ.get("COMPANION_POWER_STATE_PATH", POWER_STATE_PATH))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def legacy_scripts_root() -> Path:
|
|
77
|
+
return home() / ".claude" / "scripts"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def release_root_for(script_path: str | Path) -> Path | None:
|
|
81
|
+
path = Path(script_path).resolve()
|
|
82
|
+
parent = path.parent
|
|
83
|
+
if parent.name in {"companiond", "guardian"}:
|
|
84
|
+
root = parent.parent
|
|
85
|
+
if (root / "manifest.json").is_file():
|
|
86
|
+
return root
|
|
87
|
+
return None
|