"The permission wasn't denied to your code. It was denied to your code's identity — and a bare interpreter barely has one."
The Same Python, Two Different Answers
Second war story, same maddening shape as the last. The executor needs to read the video archive off a mounted volume — which, on modern macOS, requires the user to grant full-disk file access. You grant it, you SSH in, you run the Python, and it reads the archive perfectly. Then the service manager launches that exact same interpreter, on the same machine, and the operating system denies it file access.
The reason is that the permission system doesn't grant access to code. It grants access to an identity — a code-signed application, attributed to a responsible process. An interactive shell session carries an identity the system already trusts, and your Python inherits that context. A bare interpreter binary launched by the service manager carries no such grantable identity, so the request lands on nothing the user ever approved, and gets denied.
And it gets worse in a way that's easy to miss: even if you tried to grant access directly to that Python binary, a routine environment update can replace the binary — and the grant, which was attached to that specific binary, evaporates. You'd have built a permission that silently expires the next time you update your tooling. A grant to a swappable file is a grant with an expiry date you didn't choose and won't be told about.
The Fix: Give the Grant a Stable Identity to Live On
Recall's answer is to stop asking the OS to trust a replaceable file. The service manager hosts a small, machine-local, code-signed application bundle with a stable bundle identifier. That launcher is what the system sees; it's the responsible parent process, and it spawns the runtime Python as a child. Full-disk access is granted once, to the stable app identity — not to an interpreter that conda might replace next week. The permission now outlives every routine update to the toolchain underneath it, because it was attached to something that doesn't churn.
That's the transferable principle: permission grants attach to identity, so the identity must be stable and deliberately owned. If your grant is pinned to something that gets regenerated, upgraded, or reinstalled as a matter of routine, you haven't been granted access — you've been lent it until the next update.
The Detail That Bites Later: Forward the Signals
Inserting a launcher between the service manager and the real work creates one new obligation, and skipping it produces a nasty class of bug. When the service manager stops the job, it signals the process it knows about — the launcher. If the launcher just dies, the Python child it spawned can be orphaned: a transcription still running, still holding a lease, invisible to the lifecycle you thought you controlled. So the launcher forwards termination signals to the entire worker process group. Restart means restart, not 'restart plus a ghost still working.' Whenever you put a supervisor in front of real work, its job isn't just to start things — it's to make sure stopping actually stops everything it started.