pairling 0.2.11 → 0.2.12
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 +6 -7
- package/package.json +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairling_pairing.py +13 -7
- package/payload/mac/companiond/pairlingd.py +160 -307
- package/payload/mac/companiond/runtime_contract.py +0 -3
- package/payload/mac/companiond/runtime_manifest.py +2 -1
- package/payload/mac/companiond/runtime_paths.py +2 -6
- package/payload/mac/companiond/safety_monitor.py +56 -1
- package/payload/mac/connectd/internal/gateway/proxy.go +13 -1
- package/payload/mac/connectd/internal/gateway/proxy_test.go +24 -0
- package/payload/mac/install/bootstrap-first-run.sh +32 -1
- package/payload/mac/install/doctor.sh +43 -14
- package/payload/mac/install/install-runtime.sh +812 -50
- package/payload/mac/install/render-launchd.py +1 -28
- package/payload/mac/install/uninstall-runtime.sh +0 -3
- package/payload/mac/install/verify-payload-manifest.py +71 -0
- package/payload-manifest.json +23 -27
- package/payload/mac/guardian/companion-power-guardian.py +0 -613
- package/payload/mac/guardian/guardian_contract.py +0 -67
|
@@ -6,7 +6,6 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
|
|
8
8
|
SCHEMA_VERSION = 1
|
|
9
|
-
POWER_STATE_SCHEMA_VERSION = 1
|
|
10
9
|
RUNTIME_NAME = "pairling-mac-runtime"
|
|
11
10
|
CONTRACT_VERSION = "pairling-runtime-v1"
|
|
12
11
|
PAIRLING_CONTRACT_VERSION = CONTRACT_VERSION
|
|
@@ -14,9 +13,7 @@ COMPAT_MODE = "pairling-v1"
|
|
|
14
13
|
PORT = int(os.environ.get("PAIRLING_RUNTIME_PORT", "7773"))
|
|
15
14
|
LEGACY_PORT = 7723
|
|
16
15
|
DAEMON_LABEL = "dev.pairling.companiond"
|
|
17
|
-
GUARDIAN_LABEL = "dev.pairling.power-guardian"
|
|
18
16
|
LEGACY_TOKEN_RELATIVE_PATH = ".claude/scripts/.notify-token"
|
|
19
|
-
POWER_STATE_PATH = "/var/run/pairling-power-state.json"
|
|
20
17
|
TAILSCALE_VARIANT = "standalone"
|
|
21
18
|
AUTH_MODE = "scoped-device-bearer"
|
|
22
19
|
PAIR_SERVICE_TYPE = "_pairling-pair._tcp"
|
|
@@ -71,7 +71,7 @@ def build_runtime_info(
|
|
|
71
71
|
source_branch = os.environ.get("COMPANION_SOURCE_BRANCH", "unknown")
|
|
72
72
|
source_dirty = None
|
|
73
73
|
installed_at = os.environ.get("COMPANION_INSTALLED_AT")
|
|
74
|
-
install_root = str(script.parent.parent) if script.parent.name
|
|
74
|
+
install_root = str(script.parent.parent) if script.parent.name == "companiond" else str(script.parent)
|
|
75
75
|
source_hash = None
|
|
76
76
|
verified = False
|
|
77
77
|
verification_error = manifest_error
|
|
@@ -122,6 +122,7 @@ def public_runtime_info(info: dict[str, Any]) -> dict[str, Any]:
|
|
|
122
122
|
return {
|
|
123
123
|
"name": info.get("name") or RUNTIME_NAME,
|
|
124
124
|
"runtime_version": info.get("runtime_version"),
|
|
125
|
+
"source_revision": info.get("source_revision"),
|
|
125
126
|
"contract_version": info.get("contract_version") or CONTRACT_VERSION,
|
|
126
127
|
"compat_mode": info.get("compat_mode") or COMPAT_MODE,
|
|
127
128
|
"launchd_label": info.get("launchd_label") or DAEMON_LABEL,
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from runtime_contract import LEGACY_TOKEN_RELATIVE_PATH
|
|
9
|
+
from runtime_contract import LEGACY_TOKEN_RELATIVE_PATH
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def home() -> Path:
|
|
@@ -69,10 +69,6 @@ def token_path() -> Path:
|
|
|
69
69
|
return Path(os.environ.get("NOTIFY_TOKEN_FILE", str(home() / LEGACY_TOKEN_RELATIVE_PATH)))
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def guardian_state_path() -> Path:
|
|
73
|
-
return Path(os.environ.get("COMPANION_POWER_STATE_PATH", POWER_STATE_PATH))
|
|
74
|
-
|
|
75
|
-
|
|
76
72
|
def legacy_scripts_root() -> Path:
|
|
77
73
|
return home() / ".claude" / "scripts"
|
|
78
74
|
|
|
@@ -80,7 +76,7 @@ def legacy_scripts_root() -> Path:
|
|
|
80
76
|
def release_root_for(script_path: str | Path) -> Path | None:
|
|
81
77
|
path = Path(script_path).resolve()
|
|
82
78
|
parent = path.parent
|
|
83
|
-
if parent.name
|
|
79
|
+
if parent.name == "companiond":
|
|
84
80
|
root = parent.parent
|
|
85
81
|
if (root / "manifest.json").is_file():
|
|
86
82
|
return root
|
|
@@ -26,6 +26,10 @@ SAFETY_EVENTS_MAX_READ_BYTES = max(
|
|
|
26
26
|
64 * 1024,
|
|
27
27
|
int(os.environ.get("PAIRLING_SAFETY_EVENTS_MAX_READ_BYTES", str(1024 * 1024))),
|
|
28
28
|
)
|
|
29
|
+
SAFETY_EVENTS_DISK_WARNING_BYTES = max(
|
|
30
|
+
1024 * 1024,
|
|
31
|
+
int(os.environ.get("PAIRLING_SAFETY_EVENTS_DISK_WARNING_BYTES", str(256 * 1024 * 1024))),
|
|
32
|
+
)
|
|
29
33
|
DEFAULT_STATUS = {
|
|
30
34
|
"contract_version": SAFETY_BRIDGE_CONTRACT_VERSION,
|
|
31
35
|
"mode": "absent",
|
|
@@ -152,11 +156,28 @@ class SafetyMonitorBridge:
|
|
|
152
156
|
status["contract_version"] = SAFETY_CONTRACT_VERSION
|
|
153
157
|
if status_error:
|
|
154
158
|
status["status_error"] = status_error
|
|
155
|
-
|
|
159
|
+
live_artifact = self._live_artifact_status()
|
|
160
|
+
if live_artifact:
|
|
161
|
+
status["live_artifact"] = live_artifact
|
|
162
|
+
if live_artifact.get("source") == "event_log":
|
|
163
|
+
status["mode"] = "system_extension"
|
|
164
|
+
status["installed"] = True
|
|
165
|
+
status["approved"] = True
|
|
166
|
+
status["running"] = True
|
|
167
|
+
if status.get("visibility") == "unavailable":
|
|
168
|
+
status["visibility"] = "limited"
|
|
169
|
+
status["updated_at"] = max(
|
|
170
|
+
float(status.get("updated_at") or 0),
|
|
171
|
+
float(live_artifact.get("events_log_mtime") or 0),
|
|
172
|
+
)
|
|
173
|
+
if status.get("summary") == DEFAULT_STATUS["summary"]:
|
|
174
|
+
status["summary"] = "Pairling Safety Monitor has live event-log evidence."
|
|
175
|
+
status["status_stale"] = self._status_stale(status, loaded is not None or bool(live_artifact))
|
|
156
176
|
status["secure_mode_state"] = self._secure_mode_state(status)
|
|
157
177
|
status["guarded_mode_state"] = "guarded_deferred"
|
|
158
178
|
status["system_extension_status"] = self._system_extension_status(status)
|
|
159
179
|
status["capabilities"] = self._capabilities(status)
|
|
180
|
+
status["disk_usage_warning"] = self._disk_usage_warning(live_artifact)
|
|
160
181
|
status["evidence_test"] = self._read_evidence_test()
|
|
161
182
|
events = self.events(limit=200)
|
|
162
183
|
status["event_count"] = len(events)
|
|
@@ -362,6 +383,40 @@ class SafetyMonitorBridge:
|
|
|
362
383
|
return None, "status_unreadable"
|
|
363
384
|
return (payload, None) if isinstance(payload, dict) else (None, "status_shape_invalid")
|
|
364
385
|
|
|
386
|
+
def _live_artifact_status(self) -> dict[str, Any] | None:
|
|
387
|
+
candidates: list[dict[str, Any]] = []
|
|
388
|
+
for path in self._event_paths():
|
|
389
|
+
try:
|
|
390
|
+
stat = path.stat()
|
|
391
|
+
except OSError:
|
|
392
|
+
continue
|
|
393
|
+
if not path.is_file() or stat.st_size <= 0:
|
|
394
|
+
continue
|
|
395
|
+
candidates.append({
|
|
396
|
+
"source": "event_log",
|
|
397
|
+
"events_log_path": _redact_path(str(path), self.home),
|
|
398
|
+
"events_log_bytes": int(stat.st_size),
|
|
399
|
+
"events_log_mtime": float(stat.st_mtime),
|
|
400
|
+
"path_scope": "system" if path == self.system_events_path else "user",
|
|
401
|
+
})
|
|
402
|
+
if not candidates:
|
|
403
|
+
return None
|
|
404
|
+
candidates.sort(key=lambda item: (float(item["events_log_mtime"]), int(item["events_log_bytes"])), reverse=True)
|
|
405
|
+
return candidates[0]
|
|
406
|
+
|
|
407
|
+
def _disk_usage_warning(self, live_artifact: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
408
|
+
if not live_artifact:
|
|
409
|
+
return None
|
|
410
|
+
size = int(live_artifact.get("events_log_bytes") or 0)
|
|
411
|
+
if size < SAFETY_EVENTS_DISK_WARNING_BYTES:
|
|
412
|
+
return None
|
|
413
|
+
return {
|
|
414
|
+
"code": "safety_events_log_large",
|
|
415
|
+
"events_log_bytes": size,
|
|
416
|
+
"threshold_bytes": SAFETY_EVENTS_DISK_WARNING_BYTES,
|
|
417
|
+
"message": "Safety Monitor event log is large and should be rotated or rebuilt with the bounded writer.",
|
|
418
|
+
}
|
|
419
|
+
|
|
365
420
|
def _normalize_event(self, raw: dict[str, Any]) -> dict[str, Any]:
|
|
366
421
|
path_display = _redact_path(
|
|
367
422
|
raw.get("path") or raw.get("path_display") or raw.get("redacted_path"),
|
|
@@ -20,6 +20,7 @@ const prePairMaxBodyBytes int64 = 16 * 1024
|
|
|
20
20
|
const pairDropSmallFileMaxBodyBytes int64 = 10 * 1024 * 1024
|
|
21
21
|
const pairDropUploadChunkMaxBodyBytes int64 = 1024 * 1024
|
|
22
22
|
const peerNodeHeader = "X-Pairling-Peer-Node"
|
|
23
|
+
const internalTokenHeader = "X-Pairling-Internal-Token"
|
|
23
24
|
|
|
24
25
|
// peerProvenanceHeader tells pairlingd how connectd identified the peer node:
|
|
25
26
|
// "tagged" (the old minted tag:pairling-phone path) or "interactive" (an
|
|
@@ -233,6 +234,7 @@ func (h *Handler) rewrite(r *httputil.ProxyRequest) {
|
|
|
233
234
|
}
|
|
234
235
|
r.Out.Host = h.upstream.Host
|
|
235
236
|
r.Out.Header.Del("X-Forwarded-For")
|
|
237
|
+
r.Out.Header.Del(internalTokenHeader)
|
|
236
238
|
r.Out.Header.Del(peerNodeHeader)
|
|
237
239
|
r.Out.Header.Del(peerProvenanceHeader)
|
|
238
240
|
r.Out.Header.Del(funnelOriginHeader)
|
|
@@ -513,6 +515,9 @@ func Allowed(method, path string) bool {
|
|
|
513
515
|
if !supportedMethod(method) {
|
|
514
516
|
return false
|
|
515
517
|
}
|
|
518
|
+
if containsEscapedPathSeparator(path) {
|
|
519
|
+
return false
|
|
520
|
+
}
|
|
516
521
|
switch method {
|
|
517
522
|
case http.MethodGet:
|
|
518
523
|
return getPaths[path] || dynamicGETPath(path)
|
|
@@ -528,9 +533,17 @@ func Allowed(method, path string) bool {
|
|
|
528
533
|
}
|
|
529
534
|
|
|
530
535
|
func allowedForAnyMethod(path string) bool {
|
|
536
|
+
if containsEscapedPathSeparator(path) {
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
531
539
|
return getPaths[path] || postPaths[path] || dynamicGETPath(path) || dynamicPOSTPath(path) || dynamicPUTPath(path) || dynamicDELETEPath(path)
|
|
532
540
|
}
|
|
533
541
|
|
|
542
|
+
func containsEscapedPathSeparator(path string) bool {
|
|
543
|
+
lower := strings.ToLower(path)
|
|
544
|
+
return strings.Contains(lower, "%2f") || strings.Contains(lower, "%5c")
|
|
545
|
+
}
|
|
546
|
+
|
|
534
547
|
func localUpstream(upstream *url.URL) bool {
|
|
535
548
|
host := upstream.Hostname()
|
|
536
549
|
if host == "localhost" {
|
|
@@ -672,7 +685,6 @@ var getPaths = map[string]bool{
|
|
|
672
685
|
"/pickers/permissions": true,
|
|
673
686
|
"/pickers/resume": true,
|
|
674
687
|
"/pickers/resume/preview": true,
|
|
675
|
-
"/power-state": true,
|
|
676
688
|
"/provider-status": true,
|
|
677
689
|
"/push/status": true,
|
|
678
690
|
"/recent-projects": true,
|
|
@@ -220,6 +220,8 @@ func TestAllowedPairDropContentRouteIsGetOnlyAndPathStrict(t *testing.T) {
|
|
|
220
220
|
{http.MethodPost, "/pairdrop/files/pd_0123456789abcdef0123456789abcdef/content"},
|
|
221
221
|
{http.MethodGet, "/pairdrop/files/pd_0123456789abcdef0123456789abcdef/content/extra"},
|
|
222
222
|
{http.MethodGet, "/pairdrop/files/pd_0123456789abcdef0123456789abcdef/extra/content"},
|
|
223
|
+
{http.MethodGet, "/pairdrop/files/pd_0123456789abcdef0123456789abcdef%2fextra/content"},
|
|
224
|
+
{http.MethodGet, "/pairdrop/files/pd_0123456789abcdef0123456789abcdef%5cextra/content"},
|
|
223
225
|
}
|
|
224
226
|
for _, tc := range rejected {
|
|
225
227
|
if Allowed(tc.method, tc.path) {
|
|
@@ -449,6 +451,28 @@ func TestPairlingConnectStripsForgedPeerNodeHeader(t *testing.T) {
|
|
|
449
451
|
}
|
|
450
452
|
}
|
|
451
453
|
|
|
454
|
+
func TestPairlingConnectStripsInternalTokenHeader(t *testing.T) {
|
|
455
|
+
var forwardedHeader string
|
|
456
|
+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
457
|
+
forwardedHeader = r.Header.Get("X-Pairling-Internal-Token")
|
|
458
|
+
w.WriteHeader(http.StatusOK)
|
|
459
|
+
}))
|
|
460
|
+
defer upstream.Close()
|
|
461
|
+
handler := newTestHandlerWithMode(t, upstream.URL, 1024, nil, ExposureModePairlingConnect, nil)
|
|
462
|
+
|
|
463
|
+
req := httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/send-text", strings.NewReader(`{}`))
|
|
464
|
+
req.Header.Set("Authorization", "Bearer device-token")
|
|
465
|
+
req.Header.Set("X-Pairling-Internal-Token", "forged-internal-token")
|
|
466
|
+
rec := httptest.NewRecorder()
|
|
467
|
+
handler.ServeHTTP(rec, req)
|
|
468
|
+
if rec.Code != http.StatusOK {
|
|
469
|
+
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
|
470
|
+
}
|
|
471
|
+
if forwardedHeader != "" {
|
|
472
|
+
t.Fatalf("internal token header forwarded to upstream: %q", forwardedHeader)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
452
476
|
func TestPairlingConnectSetsPeerNodeHeaderFromResolver(t *testing.T) {
|
|
453
477
|
var forwardedHeader string
|
|
454
478
|
var forwardedProvenance string
|
|
@@ -98,6 +98,37 @@ run_step() {
|
|
|
98
98
|
printf '%s' "$status"
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
run_step_setup() {
|
|
102
|
+
# The setup step renders the guided screen, so its output must reach the
|
|
103
|
+
# controlling terminal while a copy is still written to setup.log for the audit
|
|
104
|
+
# record. This wrapper sends the live view to the controlling terminal through
|
|
105
|
+
# tee and writes a full transcript into the log. It reads the setup exit code
|
|
106
|
+
# from PIPESTATUS, the left side of the pipe, not tee, and emits only the
|
|
107
|
+
# status digit on its own stdout, so the surrounding command substitution reads
|
|
108
|
+
# a clean status. The terminal guard actually writes one empty line to
|
|
109
|
+
# /dev/tty, because the device node can test as writable when there is no
|
|
110
|
+
# controlling terminal. When that write fails, the wrapper falls back to the
|
|
111
|
+
# plain capture, so a headless launch still writes setup.log and never errors.
|
|
112
|
+
local name="$1"
|
|
113
|
+
shift
|
|
114
|
+
local log_file="$ARTIFACT_ROOT/$name.log"
|
|
115
|
+
set +e
|
|
116
|
+
if { : >/dev/tty; } 2>/dev/null; then
|
|
117
|
+
# PAIRLING_WIZARD tells install-runtime.sh to render even though its own
|
|
118
|
+
# stdout is piped here. tee writes the transcript to the log and sends its
|
|
119
|
+
# own stdout to the controlling terminal, so the screen is shown and the log
|
|
120
|
+
# stays a complete audit artifact. Stdin is inherited, so an interactive
|
|
121
|
+
# first-run still drives the recovery menu, which is gated on [ -t 0 ].
|
|
122
|
+
PAIRLING_WIZARD=1 "$@" 2>&1 | tee "$log_file" >/dev/tty
|
|
123
|
+
local status="${PIPESTATUS[0]}"
|
|
124
|
+
else
|
|
125
|
+
"$@" >"$log_file" 2>&1
|
|
126
|
+
local status=$?
|
|
127
|
+
fi
|
|
128
|
+
set -e
|
|
129
|
+
printf '%s' "$status"
|
|
130
|
+
}
|
|
131
|
+
|
|
101
132
|
run_json_step() {
|
|
102
133
|
local name="$1"
|
|
103
134
|
local output_file="$ARTIFACT_ROOT/$name.json"
|
|
@@ -109,7 +140,7 @@ run_json_step() {
|
|
|
109
140
|
printf '%s' "$status"
|
|
110
141
|
}
|
|
111
142
|
|
|
112
|
-
setup_status="$(
|
|
143
|
+
setup_status="$(run_step_setup setup "$REPO_ROOT/mac/install/install-runtime.sh" setup)"
|
|
113
144
|
doctor_before_status="$(run_json_step doctor-before "$REPO_ROOT/mac/install/doctor.sh" --first-run --json)"
|
|
114
145
|
|
|
115
146
|
pair_status="0"
|
|
@@ -60,7 +60,6 @@ home = Path.home()
|
|
|
60
60
|
|
|
61
61
|
PAIRLING_PORT = int(os.environ.get("PAIRLING_RUNTIME_PORT", "7773"))
|
|
62
62
|
PAIRLING_LABEL = "dev.pairling.companiond"
|
|
63
|
-
PAIRLING_GUARDIAN_LABEL = "dev.pairling.power-guardian"
|
|
64
63
|
PAIRLING_CONNECTD_LABEL = "dev.pairling.connectd"
|
|
65
64
|
PAIRLING_PTYBROKER_LABEL = "dev.pairling.ptybroker"
|
|
66
65
|
TEAM_ID = os.environ.get("PAIRLING_TEAM_ID", os.environ.get("PAIRLING_CONNECTD_TEAM_ID", "965AVD34A3"))
|
|
@@ -77,7 +76,7 @@ PAIR_ROOT = APP_SUPPORT / "pair"
|
|
|
77
76
|
USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_LABEL}.plist"
|
|
78
77
|
CONNECTD_USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_CONNECTD_LABEL}.plist"
|
|
79
78
|
PTYBROKER_USER_PLIST = home / "Library" / "LaunchAgents" / f"{PAIRLING_PTYBROKER_LABEL}.plist"
|
|
80
|
-
|
|
79
|
+
CLAUDE_INJECTOR = home / "Applications" / "ClaudeInjector.app" / "Contents" / "MacOS" / "ClaudeInjector"
|
|
81
80
|
|
|
82
81
|
sys.path.insert(0, str(repo_root / "mac" / "companiond"))
|
|
83
82
|
from pairling_connectd_status import fetch_connectd_status, redacted_connectd_summary
|
|
@@ -175,6 +174,8 @@ def detected_tailnet_ip() -> str | None:
|
|
|
175
174
|
|
|
176
175
|
|
|
177
176
|
def permission_readiness() -> dict:
|
|
177
|
+
helper_installed = CLAUDE_INJECTOR.exists()
|
|
178
|
+
grantee_path = str(CLAUDE_INJECTOR if helper_installed else Path("/usr/bin/osascript"))
|
|
178
179
|
return {
|
|
179
180
|
"ios_local_network": {
|
|
180
181
|
"required_for": ["bonjour_pairing", "lan_route_validation"],
|
|
@@ -187,10 +188,16 @@ def permission_readiness() -> dict:
|
|
|
187
188
|
"mac_accessibility": {
|
|
188
189
|
"required_for": ["terminal_ui_synthesis"],
|
|
189
190
|
"status": "not_required_until_terminal_control",
|
|
191
|
+
"grantee_path": grantee_path,
|
|
192
|
+
"helper_installed": helper_installed,
|
|
193
|
+
"helper_path": str(CLAUDE_INJECTOR),
|
|
194
|
+
"doctor_probe": "reports_required_grantee",
|
|
190
195
|
},
|
|
191
196
|
"mac_automation": {
|
|
192
197
|
"required_for": ["terminal_app_control"],
|
|
193
198
|
"status": "not_required_by_default",
|
|
199
|
+
"grantee_path": grantee_path,
|
|
200
|
+
"doctor_probe": "reports_required_grantee",
|
|
194
201
|
},
|
|
195
202
|
"privacy_database": "not_modified",
|
|
196
203
|
}
|
|
@@ -250,6 +257,37 @@ def ptybroker_status_rpc() -> tuple[dict | None, str | None]:
|
|
|
250
257
|
return None, f"{type(exc).__name__}: {exc}"
|
|
251
258
|
|
|
252
259
|
|
|
260
|
+
def safety_monitor_status() -> dict:
|
|
261
|
+
# Report the live SafetyMonitorBridge status, not a guess from local files.
|
|
262
|
+
# The bridge is imported from the staged companiond, the same copy the daemon
|
|
263
|
+
# runs. When no runtime is staged or the import fails, report a structured
|
|
264
|
+
# value with installed false, so the doctor JSON stays pure and never leaks a
|
|
265
|
+
# traceback. The bridge itself defaults to installed false when the future
|
|
266
|
+
# PairlingSafety.app is not present, which is today's true value.
|
|
267
|
+
try:
|
|
268
|
+
sys.path.insert(0, str(CURRENT / "companiond"))
|
|
269
|
+
from safety_monitor import SafetyMonitorBridge
|
|
270
|
+
bridge = SafetyMonitorBridge(APP_SUPPORT, home)
|
|
271
|
+
status = bridge.status()
|
|
272
|
+
return {
|
|
273
|
+
"installed": bool(status.get("installed")),
|
|
274
|
+
"full_disk_access": status.get("full_disk_access") or "unknown",
|
|
275
|
+
"system_extension_status": status.get("system_extension_status"),
|
|
276
|
+
"secure_mode_state": status.get("secure_mode_state"),
|
|
277
|
+
"live_artifact": status.get("live_artifact"),
|
|
278
|
+
"disk_usage_warning": status.get("disk_usage_warning"),
|
|
279
|
+
"summary": status.get("summary") or "",
|
|
280
|
+
"source": "live_bridge_status",
|
|
281
|
+
}
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
return {
|
|
284
|
+
"installed": False,
|
|
285
|
+
"full_disk_access": "unknown",
|
|
286
|
+
"source": "live_bridge_status",
|
|
287
|
+
"error": str(exc)[:200],
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
253
291
|
def ptybroker_deployment_status(*, launchd_loaded: bool) -> dict:
|
|
254
292
|
desired = desired_ptybroker_identity()
|
|
255
293
|
base = {
|
|
@@ -387,9 +425,8 @@ if manifest:
|
|
|
387
425
|
"daemon_label": launchd.get("daemon_label"),
|
|
388
426
|
"ptybroker_label": launchd.get("ptybroker_label"),
|
|
389
427
|
"connectd_label": launchd.get("connectd_label"),
|
|
390
|
-
"guardian_label": launchd.get("guardian_label"),
|
|
391
428
|
}
|
|
392
|
-
add("launchd_labels", launchd.get("daemon_label") == PAIRLING_LABEL and launchd.get("ptybroker_label") == PAIRLING_PTYBROKER_LABEL and launchd.get("connectd_label") == PAIRLING_CONNECTD_LABEL
|
|
429
|
+
add("launchd_labels", launchd.get("daemon_label") == PAIRLING_LABEL and launchd.get("ptybroker_label") == PAIRLING_PTYBROKER_LABEL and launchd.get("connectd_label") == PAIRLING_CONNECTD_LABEL, "error", "Manifest launchd labels are Pairling labels.", launchd_evidence)
|
|
393
430
|
mismatches = []
|
|
394
431
|
for item in manifest.get("files") or []:
|
|
395
432
|
rel = item.get("path")
|
|
@@ -413,15 +450,13 @@ else:
|
|
|
413
450
|
|
|
414
451
|
compile_targets = [
|
|
415
452
|
repo_root / "mac" / "install" / "render-launchd.py",
|
|
416
|
-
repo_root / "mac" / "guardian" / "guardian_contract.py",
|
|
417
|
-
repo_root / "mac" / "guardian" / "companion-power-guardian.py",
|
|
418
453
|
]
|
|
419
454
|
compile_errors = []
|
|
420
455
|
for target in compile_targets:
|
|
421
456
|
code, out, err = run(["python3", "-m", "py_compile", str(target)])
|
|
422
457
|
if code != 0:
|
|
423
458
|
compile_errors.append(f"{target}: {err or out}")
|
|
424
|
-
add("lifecycle_sources_compile", not compile_errors, "error", "Lifecycle
|
|
459
|
+
add("lifecycle_sources_compile", not compile_errors, "error", "Lifecycle sources compile." if not compile_errors else "Lifecycle compile failed.", compile_errors)
|
|
425
460
|
|
|
426
461
|
ok, evidence = writable_dir(APP_SUPPORT)
|
|
427
462
|
add("app_support_writable", ok, "error", "App support directory is writable.", evidence)
|
|
@@ -545,12 +580,6 @@ try:
|
|
|
545
580
|
except Exception as exc:
|
|
546
581
|
add("ptybroker_launchagent_plist", False, "error", f"Pairling PTY broker LaunchAgent plist unreadable: {type(exc).__name__}: {exc}", str(PTYBROKER_USER_PLIST))
|
|
547
582
|
|
|
548
|
-
try:
|
|
549
|
-
payload = load_plist(SYSTEM_PLIST)
|
|
550
|
-
add("guardian_plist", payload.get("Label") == PAIRLING_GUARDIAN_LABEL, "warning", "Pairling guardian LaunchDaemon is rendered/installed.", {"label": payload.get("Label")})
|
|
551
|
-
except Exception as exc:
|
|
552
|
-
add("guardian_plist", False, "warning", f"Pairling guardian LaunchDaemon is not installed: {type(exc).__name__}: {exc}", str(SYSTEM_PLIST))
|
|
553
|
-
|
|
554
583
|
code, out, err = run(["launchctl", "print", f"gui/{os.getuid()}/{PAIRLING_LABEL}"])
|
|
555
584
|
add("launchagent_loaded", code == 0 and "state = running" in out, "error", "Pairling LaunchAgent is running." if code == 0 else "Pairling LaunchAgent is not loaded.", (out or err)[:2000])
|
|
556
585
|
add("launchagent_loaded_from_current", str(CURRENT / "companiond" / "pairlingd.py") in out, "error", "Loaded Pairling LaunchAgent uses runtime/current.", out[:2000])
|
|
@@ -790,13 +819,13 @@ first_run = {
|
|
|
790
819
|
result = {
|
|
791
820
|
"ok": not errors,
|
|
792
821
|
"product": "Pairling",
|
|
822
|
+
"safety_monitor": safety_monitor_status(),
|
|
793
823
|
"schema_version": 1,
|
|
794
824
|
"contract_version": "pairling-runtime-v1",
|
|
795
825
|
"runtime": {
|
|
796
826
|
"name": "pairlingd",
|
|
797
827
|
"port": PAIRLING_PORT,
|
|
798
828
|
"launchd_label": PAIRLING_LABEL,
|
|
799
|
-
"guardian_label": PAIRLING_GUARDIAN_LABEL,
|
|
800
829
|
},
|
|
801
830
|
"ptybroker": ptybroker_deployment,
|
|
802
831
|
"paths": {
|