Skip to content

native2: approve a background agent by its signed program (#194) - #218

Merged
iamnbutler merged 4 commits into
mainfrom
fix-bg-callers
Oct 25, 2026
Merged

iamnbutler merged 4 commits into
mainfrom
fix-bg-callers

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Fixes #194

Implements the owner's decision on #194.

  • Who is the caller. When no app is among the client's ancestors and they reach launchd (a claude --bg job: node → claude bg-spare → claude bg-pty-host → claude daemon, ppid 1), the caller is the nearest program that isn't a launcher and is signed by a developer team, e.g. Claude Code. Its approval is keyed team:<team>:<identifier> as before, so it's the same entry as the foreground claude-code approval. Chains with an app (a terminal) are unchanged.
  • Prompt. A caller that isn't inside an .app gets "Allow the program name to control this Mac?" and a line saying it's a command-line program, not an app, and that every agent it runs, in a terminal or in the background, could use Desktop Tools. (app.swift is copied to native/sources/app/ too, since scripts.test.mjs keeps the two identical.)
  • No signed program. The refusal is still CALLER_NOT_APPROVED with no prompt, but now names the processes it found and why nothing can be approved: …its client's parent processes (bash > perl) reach launchd with no app and no program signed by a developer team among them…, and says to run from a terminal app or a signed program.
  • Hardening. A caller's team counts only when the code satisfies anchor apple generic and certificate leaf[subject.OU] = <team>. Before this, any valid signature counted, so a self-signed certificate with a forged OU could take an approved team:… key. Legitimate Developer ID and Apple Development callers keep the same keys.
  • Docs. docs/app.md covers background agents, and it and the README say that background jobs don't load nvm, so give npx/node by absolute path (or use the app's desktop-tools-mcp).

Couldn't reproduce the reported "couldn't identify". On this Mac, a real claude --bg job running the dev app's client was served on main's code as well as this branch's: the chain resolved to claude-code, which was already approved. So the original failure must have had another cause, perhaps a parent whose executable couldn't be read. Any remaining "couldn't identify" case now falls through to the generic refusal.

Checked: swift build -c release, swift test (refusal text tests), npm test. On the dev app (npm run dev-app -- --native2): a real claude --bg job is served as claude-code; a foreground call still works; a client whose parent is launchd and a client under a detached perl get the new refusal text. The new "program" prompt wasn't shown live, since that would put a prompt on the user's screen for an unapproved signed binary.

When no app is among the client's ancestors and they reach launchd, as for
a claude --bg job, the caller is the nearest team-signed program that isn't
a launcher, such as Claude Code, keyed by team and identifier as before.
The prompt says it is a program, not an app. With no such program the
refusal says why and what to do. A team counts only from an Apple-issued
certificate.

Why: native2/Sources/DesktopHost/Admission.swift#func resolution | with no app above the client and the walk reaching launchd, only a team-signed program may be the caller, since an unsigned one or a bare path can't be told apart from anything else the user runs in the background; owner decision on #194
Why: native2/Sources/DesktopHost/Admission.swift#func identify | a caller's team counts only when it satisfies anchor apple generic with that leaf OU: any signer can write a team ID into a self-signed certificate, and approvals are keyed team:identifier
@iamnbutler
iamnbutler merged commit 5aac41f into main Oct 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Code background jobs are refused with CALLER_NOT_APPROVED and never get an approval prompt

1 participant