Files
Jinwoo-H 27dabcbe90 docs(macos): fold PostHog field data into the STA-7948 plan and drop the helper probe
daemon_pty_cwd_denied fired for 1,438 users in 21 days (2.8% of users adopting
a different-version daemon, 96% on Documents/Desktop/Downloads), so the daemon's
existing verdict already observes the failure and the helper probe for old
daemons has no population left to cover. A fresh same-version daemon ended the
denial for about 69% of observable users; the notice copy now tells the rest to
re-allow the folder.
2026-09-21 01:25:33 -04:00

204 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TCC recovery detector validation — 2026-09-14
Scope: validate the proposed old-daemon-compatible transport and lifecycle mechanics, then
independently review the design. This is not a reproduction of TCC poisoning.
## Evidence
The harness launches its own isolated daemon process using `startDaemon`, `DaemonClient`, and
the production `createPtySubprocess` path. It creates fresh temporary runtime directories and
uses a compiled C executable as `shellOverride`, with no command input. The parent driver is
Node, not the installed Electron app. No existing user daemon was restarted, no user session
was touched, and no TCC permission or app bundle was modified.
Both runs explicitly reported the actual launch strategy: `direct` and `wrapped` respectively.
| Case | Helper result | Driver result | Cleanup |
| --- | --- | --- | --- |
| Readable directory with spaces, quote, dollar sign, backticks, newline | errno 0 | ok | No live or retained diagnostic session |
| Missing directory | errno 2 | ENOENT | No live or retained diagnostic session |
| Fixture directory chmod 000 | errno 13 | EACCES | No live or retained diagnostic session |
| Sleeping helper, client kills its session | No access verdict inferred | Not applicable | PID absence confirmed with ESRCH |
| Sleeping helper, no client kill | No access verdict inferred | Not applicable | Five-second helper alarm; PID absence confirmed |
Both daemon processes exited with code 0 after the harness shut them down. Fixture directory
permissions were restored after each run. The small temporary runtime directories were retained
for inspection; generated bundle removal does not affect the retained source.
The first harness iteration used the wrong event field (`payload.sessionId` instead of
`event.sessionId`); the second attempted to kill a session already reaped after normal exit.
Both harness issues were corrected before the successful runs. Normal cleanup tolerates only
the specific already-reaped error, not arbitrary failures.
Existing regression suites:
```sh
ORCA_BACKGROUND_LAUNCH=1 node_modules/.bin/vitest run --config config/vitest.config.ts \
src/main/daemon/terminal-host-cwd-readability.test.ts \
src/main/daemon/daemon-adoption-telemetry-event.test.ts \
src/main/daemon/daemon-tcc-attribution.test.ts
```
Result: **3 files passed, 22 tests passed**.
Historical source check: `git show a7fda48fe3^:src/main/daemon/types.ts` and the corresponding
`pty-subprocess/shell-launch-plan.ts` confirmed that `shellOverride`, `env`, and Unix executable
launch support existed before cwd-readability telemetry. No historical binary was executed.
## Reproduce the transport checks
From this worktree, with host-compatible node-pty already available:
```sh
TCC_PROBE_BUILD_DIR=$(mktemp -d /tmp/orca-tcc-validation.XXXXXX)
clang -Wall -Wextra -O2 docs/plans/validation/tcc-directory-probe.c \
-o "$TCC_PROBE_BUILD_DIR/directory-probe"
node_modules/.bin/esbuild docs/plans/validation/tcc-probe-validation.ts \
--bundle --platform=node --format=cjs --packages=external \
--outfile=docs/plans/validation/tcc-probe-validation.cjs
ORCA_BACKGROUND_LAUNCH=1 node docs/plans/validation/tcc-probe-validation.cjs \
"$TCC_PROBE_BUILD_DIR/directory-probe"
ORCA_BACKGROUND_LAUNCH=1 ORCA_PROBE_WRAPPED=1 \
node docs/plans/validation/tcc-probe-validation.cjs "$TCC_PROBE_BUILD_DIR/directory-probe"
```
The harness is a macOS experiment, not shipping code. Wrapped mode requests the production
login preflight; check the emitted actual strategy, since an unavailable login wrapper can
fall back to direct. The helper contains a test-only stall switch. Production packaging,
strict protocol parsing, queue limits, absolute deadlines, and crash-path cleanup need separate
implementation and tests.
## Independent review
A second agent reviewed the current protocol, shell launch, session listing, login wrapper,
and environment handling. It recommended using the tiny executable instead of a diagnostic
shell command, avoiding permanent success caching, and treating old-reader visibility and
late-create cleanup explicitly. Those findings are incorporated in the design.
Its final review also caught a trigger gap: checks must include failed terminal admission,
since permission denial can prevent creation. The rewritten design includes that correction.
## What this does not prove
- No known poisoned daemon was available in the validation fixture.
- chmod denial is POSIX permission behavior, not TCC; both tested sides were denied.
- The unsigned prototype does not establish the final signed helper's TCC attribution.
- A child can acquire different attribution under the login wrapper or executable identity.
- Current-daemon protocol execution plus historical source inspection is not old-binary proof.
- The helper alarm bounds an ordinary sleep, not an uninterruptible filesystem operation.
- No actual restart or rendered notice was tested; there is no production UI change yet.
The shipping gate remains: affected ordinary terminal denied, final packaged helper denied,
Electron main succeeds, then terminal/helper reads succeed after explicit restart.
---
# 2026-09-21 live evidence (production adopted daemon)
Scripts: [`live-2026-09-21/`](live-2026-09-21/). Full raw report (TCC rows, ps trees, logs) is
kept outside the repo at `~/orca-lanes/sta-7948-evidence-20260921/report.md` on the collecting
machine because it contains account paths.
## A. `access(2)` versus enumeration under TCC (`access_vs_scandir.py`)
Run as a launchd-owned `/usr/bin/python3` via `launchctl submit` (responsible pid = itself, no
TCC row). No `tccutil`, no window; the job was removed afterwards.
| Target | `access(R_OK\|X_OK)` | `stat` | `scandir` |
| --- | --- | --- | --- |
| `~/Documents` | **True** | ok | **EPERM** |
| `~/Library/Safari` (Full Disk Access class) | False | ok | EPERM |
| `~/Library/Mail` (Full Disk Access class) | False | ok | EPERM |
| `/tmp` | True | ok | ok |
The Documents run was executed twice with identical output. Conclusion: for the
SystemPolicyDocumentsFolder class, `accessSync(R_OK|X_OK)` passes while enumeration is denied, so
the daemon's current `cwdReadableByDaemon` verdict cannot observe the incident's failure.
## B. Production daemon, healthy state (`probe-daemon.mjs`)
Target: the live adopted daemon (pid 1232, protocol v36, spawned by app 1.4.207-adhoc while the
running app was 1.4.206; `spawnerExecPath` `/Applications/Orca.app/Contents/MacOS/Orca`). The
client speaks the real NDJSON protocol on the control and stream sockets with its own `clientId`,
creates one short-lived session per target with a `sta7948-evidence-*` session id, reads the
probe line, kills the session, and confirms it is absent from `listSessions`.
| Target | daemon `cwdReadableByDaemon` (accessSync) | login-wrapped shell `scandir` | control shell |
| --- | --- | --- | --- |
| `~/Documents` | true | ok (5 entries) | ok |
| `/tmp` | true | ok | ok |
| `~/Library/Safari` | true | ok (36 entries) | ok |
Five sessions were created; all exited 0; session count was 43 before and after. No process was
killed, signalled, or restarted; no window was shown.
## C. TCC responsible-process identity (`resp.c`)
`responsibility_get_pid_responsible_for_pid` (dlsym) for the process tree:
```
86248 -> 86248 Orca main
25437 -> 86248 Orca Helper (Renderer), child of Orca main (probe control: children resolve to main)
86324 -> 86248 /usr/bin/log forked by Orca main (probe control)
1232 -> 1232 production daemon (Orca Helper)
123 -> -1 /usr/bin/login (root; query not permitted)
162 -> 162 -/bin/zsh under that login
73362 -> 73362 -/bin/zsh under another login
73502 -> 73502 claude under that zsh
```
The daemon and every login-wrapped shell resolve to themselves, never to Orca main. Yet the
shells read `~/Library/Safari`, which only `com.stablyai.orca` holds Full Disk Access for
(`com.stablyai.orca.helper`, the daemon's own identifier, has no TCC row). The shells therefore
carry Orca's grant through the daemon, not through a pid-level link this API exposes. The
`ps` tree is `daemon -> /usr/bin/login -> -/bin/zsh -> agent CLIs`; the bash trampoline `exec`s
away and never appears.
## D. Other facts
- All 1734 `macos-tcc-pty-spawn` events in the local daemon log carry `strategy: "wrapped"`.
- `daemon_pty_cwd_denied` has never fired on this machine and no `cwdReadableByDaemon=false`
appears in `daemon.log` or `main.trace.ndjson`; per section A this is uninformative for the
Documents class.
- `/Applications/Orca.app` and `Orca Helper.app` are signed Developer ID, TeamIdentifier
`6CX3WHS9HZ`, hardened runtime.
## What this still does not prove
- The broken state was not observed; sections B and C describe the healthy state only.
- Whether the daemon's in-process `opendir` and its shells' reads diverge when the lineage is
broken (gate G1 in the design).
- Whether restart alone recovers, or restart plus `tccutil reset` and re-allow is required (G1).
---
# PostHog field data (queried 2026-09-21, 21-day window)
`daemon_pty_cwd_denied` fires only on proven divergence: daemon `accessSync` denied, app
`accessSync` succeeded, macOS only (#18043, shipped 2026-09-01).
| cwd_class | app_version_match | events | users |
| --- | --- | ---: | ---: |
| documents | different | 5,674 | 863 |
| desktop | different | 2,379 | 404 |
| outside-home | different | 480 | 58 |
| downloads | different | 369 | 79 |
| documents | same | 113 | 33 |
| desktop | same | 113 | 25 |
| other-home | different | 25 | 8 |
- Distinct denied users: 1,438 (1,374 in documents/desktop/downloads). Users who adopted a
different-version daemon in the window: 50,876. Denial rate among them: 2.8%.
- Persistence: 910 users denied on one day, 413 on 2–3 days, 115 on 4 or more days.
- Daily denied users ramped from single digits on 2026-09-07 to about 300/day by 2026-09-16 as
the telemetry release rolled out, then about 110–135/day over the weekend.
- Remedy signal: among denied users with a later same-version `daemon_adopted` (a daemon forked
by the current app), 151 had no further denial and 68 were denied again (about 69% / 31%).
1,219 had no same-version adoption inside the window.
- `daemon_adopted` with `tcc_attribution=severed` (the existing notice's trigger): 45 users. The
cwd-denial population is about 30 times larger.
Consequence: the daemon's existing `accessSync` verdict does observe the failure in the field.
The grant-less launchd probe (section A above) shows a second TCC mode on `~/Documents` where
`access()` passes and `opendir` fails; the two modes are different TCC states, and enumeration
covers both.