feat(chat): surface macOS TCC denials as in-card affordance#406
Merged
Conversation
When run_code (or a future file-io variant) hits "Operation not permitted"
against a TCC-protected user folder (~/Downloads, ~/Desktop, ~/Documents),
the chat now renders a yellow "macOS permission needed" card with the
attempted path and a one-click deeplink to System Settings → Privacy &
Security → Files & Folders. For sub-paths under the protected root we also
offer a copy-to-clipboard mv command. The model used to be left dumping
the raw shell error at the user; now it has a structured affordance to
point at.
Scope is macOS only — on Linux "Operation not permitted" usually means
a real chmod/ACL issue and the raw error is the right signal. The
detector is gated on process.platform === "darwin".
The card is the fallback for the already-denied state (user previously
clicked Don't Allow, or revoked via System Settings — auth_value=0 in
TCC.db). The first-encounter path is still handled by macOS's native
consent dialog.
QA validation (real-TCC repro on a Tahoe VM with the daemon's Downloads
grant revoked via tccutil) found two bugs that are folded into this commit:
1. UI card never rendered: tool-burst wraps tool calls in a collapsed
<details>, and needsUserAction didn't recognize tcc_denied so the
burst stayed closed. The card was in the DOM but invisible —
embarrassingly, the model could see tcc_denied in its tool result
JSON and would describe "a shortcut button above" that wasn't
actually on screen.
2. No-op mv suggestion: when the user attempted the protected root
itself (ls ~/Downloads), the suggested action was
mv '~/Downloads' '~/Downloads'. Now suppressed when attemptedPath
=== protectedRoot.
…dexedAccess CI flagged two real issues on PR #406: 1. test (vitest): tcc-denied-card.test.ts couldn't load because importing `Button, IconSmall from "@atlas/ui"` transitively pulls in the table barrel → `@tanstack/svelte-table` → an extensionless `createTable.svelte` re-export that Node ESM can't resolve under vitest. The SvelteKit build pipeline handles this fine; only the test runtime trips. Fix: ditch the barrel import and render plain HTML — the card already owns its styling so there's no design-system regression. 2. type-check (deno check): `result?.actions[N].payload` is `undefined` when the array index is out of range (noUncheckedIndexedAccess). Added the second `?.` chain on the four affected lines in tcc-detect.test.ts. Both fixes are local to this PR's surface — no changes to the detector itself or the runtime behavior. All 16 unit tests still pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When the chat agent's
run_code(or file-io) tool hitsOperation not permittedagainst a TCC-protected user folder (~/Downloads,~/Desktop,~/Documents), the chat now renders a yellow macOS permission needed card with a one-click deeplink to System Settings → Privacy & Security → Files & Folders.Before: agent saw the raw shell error, dumped it at the user, and asked them to debug it.
After: structured affordance the model can point at — and a clickable surface the user can act on directly.
Scope: macOS only. On Linux/Windows the same error usually means a real ACL issue and the raw error is the right signal — the detector is gated on
process.platform === "darwin".Edge case clarification: this card is the fallback for the already-denied state (user previously clicked Don't Allow, or revoked via System Settings →
auth_value=0inTCC.db). The first-encounter path is still handled by macOS's native consent dialog.What's in the change
packages/system/agents/workspace-chat/tools/tcc-detect.ts— pure detector. Recognizes three error formats (find:,ls:, PythonPermissionError), returns a structuredTccDenialwith deeplink + optionalmvsuggestion. 12 unit tests.code-exec.ts— attachestcc_denied?: TccDenialtoRunCodeSuccesswhen the script exited non-zero and the stderr matches.file-io.ts— same wiring via a sharedtoFileErrhelper. Forward-defensive — the existing path-resolution gates keep these tools inside the scratch dir, so TCC isn't reachable today.tools/agent-playground/.../tcc-denied-card.svelte— the actual card component, 4 SSR tests.tool-call-card.svelte— typedreadTccDeniedextractor + render branch.tool-call-utils.ts— extendsneedsUserActionto recognizetcc_denied, which is what auto-opens thetool-burst<details>so the card is actually visible.QA
Real-TCC reproduction on a Tahoe VM (192.168.64.17) with the daemon's Downloads grant revoked via
tccutil reset:ls ~/Downloads→ card renders with eyebrow, guidance, path, Settings buttonls ~/Downloads/qa-test→ both buttons render; Move button writes correctmv '<src>' '<dst>'to clipboardx-apple.systempreferences:deeplink (structural)ls /tmp(success) → no false cardls /nonexistent(ENOENT) → no false cardQA found two bugs that are folded into this PR:
tool-burstwraps tool calls in a collapsed<details>andneedsUserActiondidn't recognizetcc_denied, so the card was in the DOM but hidden. Embarrassingly, the model could seetcc_deniedin its tool result JSON and would describe "a shortcut button above" that wasn't actually on screen. Fix: extendneedsUserAction.mvsuggestion when user attempted the protected root directly (mv '~/Downloads' '~/Downloads'). Fix: only push the Move action whenattemptedPath !== root. New unit test locks this in.Test plan
tccutil reset SystemPolicyDownloadsFolder ai.hellofriday.studio+ click Don't Allow when prompted), then ask the chatrun ls -la ~/Downloads— verify yellow card appears~/Desktopand~/Documentsto cover the other two protected dirsls /tmp) and ENOENT runs (ls /nonexistent) don't trigger the affordance