Repository navigation
fix(context-drop): TCC-stable AX trust + flipped priority + region-capture fallback - #902
Merged
Merged
Conversation
sonichi
force-pushed
the
fix/context-drop-electron-via-proxy
branch
from
May 19, 2026 19:45
2900730 to
ef62646
Compare
sonichi
force-pushed
the
fix/context-drop-electron-via-proxy
branch
from
May 19, 2026 19:59
ef62646 to
5cd2ad4
Compare
…pture fallback Folds three changes onto the ax-read subprocess approach (PR #891 groundwork) to make ⌃C reliable across rebuilds: 1. **AXIsProcessTrustedWithOptions(prompt:true) at startup.** When Sutando.app's codesign identity changes (ad-hoc → cert, rename, rotate), the prior TCC Accessibility grant orphans silently — macOS returns AX errors without re-prompting. Empirical case 2026-05-19: Sutando.app was re-signed; AXIsProcessTrusted=false on first launch despite the prior grant still appearing toggled-on in System Settings. Adding the prompt call at startup forces macOS to re-bind the entry to the current binary on first AX use, or pop the standard "Sutando wants Accessibility" dialog if no matching entry exists. Idempotent when already trusted. Result logged so future drift is grep-able from session 0. 2. **Flip dropContext priority: ax-read FIRST, clipboard image second.** Original order checked clipboard PNG/TIFF BEFORE attempting AX read. Result: any lingering screenshot in the clipboard pre-empted text- selection drops, so ⌃C in Discord with a selection would silently drop yesterday's screenshot instead. The dominant ⌃C use case is "drop the highlighted text" — so ax-read runs first; only when it returns empty (`path=none`) do we fall through to the clipboard image branches. 3. **screencapture -i -c last-resort fallback.** When ax-read returns empty AND clipboard has no image AND legacy AX returns empty AND simulated Cmd+C doesn't advance pasteboard changeCount, instead of giving up with "Nothing selected" we now launch `screencapture -i -c`. macOS draws the crosshair, user drag-selects a region (or Esc to cancel). On completion the resulting PNG/TIFF clipboard content is captured as an image drop. Per Chi 2026-05-19: every ⌃C now has SOME path to producing a drop instead of dead-ending at "Nothing selected." Also requires (out-of-band, not in this PR) codesigning Sutando.app with an embedded designated requirement so TCC keys on bundle identifier alone, surviving cdhash churn across rebuilds: codesign --requirements '=designated => identifier "com.sutando.menubar"' Verified: rebuilt binary with different cdhash + same identifier passes `AXIsProcessTrusted=true` on first run after one initial grant. Tested locally: - ⌃C with text selected in Discord → drops via ax-read (path=ax) - ⌃C with image in clipboard, text selected → text wins (was: image won) - ⌃C with nothing selected anywhere → screencapture crosshair fires - Esc during screencapture → "Cancelled — nothing dropped" Supersedes previous proxy-via-web-client approach on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
force-pushed
the
fix/context-drop-electron-via-proxy
branch
from
May 19, 2026 20:04
5cd2ad4 to
58d9e02
Compare
This was referenced May 19, 2026
qingyun-wu
added a commit
that referenced
this pull request
May 19, 2026
…904) * fix(bridges): team/other codex output via staging path + atomic move The current team/other tier system-instructions inject: codex exec --sandbox read-only -o {RESULTS_DIR}/task-{id}.txt -- ... …and tell the agent: "if codex exits non-zero OR the output file does not appear after the command finishes, treat as a failure and apply the fallback rule." That check is racy. Sequence observed today: 1. Codex completes, writes results/task-{id}.txt (exit 0). 2. Bridge polls results/, picks the file up, delivers to channel, moves to results/archive/. 3. Agent's post-exec `ls results/task-{id}.txt` happens AFTER step 2, sees "no such file," writes the 'Sandbox unavailable; refusing non-owner task.' fallback to results/task-{id}.txt. 4. Bridge delivers that second message → user receives both the real codex reply AND the spurious fallback. Caught it after Lucy's PR-skill design question (full codex review delivered, then a "Sandbox unavailable" follow-up at ~10s) and MacBook's PR #902 review ping (same pattern, real review at 19:53 then fallback ~10s later in #bot2bot-trio). Fix: rewrite both team-tier and other-tier system-instructions to use a two-stage flow: Stage 1: codex exec ... -o /tmp/sutando-codex-staging-{id}.txt -- ... Stage 2: if codex exit 0 AND staging non-empty: mv /tmp/sutando-codex-staging-{id}.txt {RESULTS_DIR}/task-{id}.txt else: write 'Sandbox unavailable; refusing non-owner task.' to {RESULTS_DIR}/task-{id}.txt directly. The bridge poller only ever sees the final file (post-mv) — no in-flight half-written results, no phantom-missing race. The two existing failure modes (codex non-zero exit, codex produced no output) collapse into one "empty/missing staging file" check evaluated BEFORE the bridge ever sees anything in results/. No bridge-side code changes; the poller and archive logic are unchanged. The only diff is the system-instructions template injected into team/other tier task files. * fix(bridges): same-fs staging path for guaranteed atomic mv Per qingyun-sutando review note #2 on #904: staging in /tmp/ + mv into {RESULTS_DIR}/ is only atomic when both share a filesystem. Today both live on the system disk so this holds. Future-proof if anyone moves $SUTANDO_WORKSPACE to an external volume: stage to {RESULTS_DIR}/.codex-staging-{id}.txt instead. Same-filesystem mv is guaranteed atomic regardless of workspace mount point. Leading dot keeps the staging file out of the results-poll glob (bridge filters by 'task-' / 'proactive-' prefix; .codex-staging-* doesn't match either). --------- Co-authored-by: Qingyun Wu <qingyun0327@gmail.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…pture fallback (sonichi#902) Folds three changes onto the ax-read subprocess approach (PR sonichi#891 groundwork) to make ⌃C reliable across rebuilds: 1. **AXIsProcessTrustedWithOptions(prompt:true) at startup.** When Sutando.app's codesign identity changes (ad-hoc → cert, rename, rotate), the prior TCC Accessibility grant orphans silently — macOS returns AX errors without re-prompting. Empirical case 2026-05-19: Sutando.app was re-signed; AXIsProcessTrusted=false on first launch despite the prior grant still appearing toggled-on in System Settings. Adding the prompt call at startup forces macOS to re-bind the entry to the current binary on first AX use, or pop the standard "Sutando wants Accessibility" dialog if no matching entry exists. Idempotent when already trusted. Result logged so future drift is grep-able from session 0. 2. **Flip dropContext priority: ax-read FIRST, clipboard image second.** Original order checked clipboard PNG/TIFF BEFORE attempting AX read. Result: any lingering screenshot in the clipboard pre-empted text- selection drops, so ⌃C in Discord with a selection would silently drop yesterday's screenshot instead. The dominant ⌃C use case is "drop the highlighted text" — so ax-read runs first; only when it returns empty (`path=none`) do we fall through to the clipboard image branches. 3. **screencapture -i -c last-resort fallback.** When ax-read returns empty AND clipboard has no image AND legacy AX returns empty AND simulated Cmd+C doesn't advance pasteboard changeCount, instead of giving up with "Nothing selected" we now launch `screencapture -i -c`. macOS draws the crosshair, user drag-selects a region (or Esc to cancel). On completion the resulting PNG/TIFF clipboard content is captured as an image drop. Per Chi 2026-05-19: every ⌃C now has SOME path to producing a drop instead of dead-ending at "Nothing selected." Also requires (out-of-band, not in this PR) codesigning Sutando.app with an embedded designated requirement so TCC keys on bundle identifier alone, surviving cdhash churn across rebuilds: codesign --requirements '=designated => identifier "com.sutando.menubar"' Verified: rebuilt binary with different cdhash + same identifier passes `AXIsProcessTrusted=true` on first run after one initial grant. Tested locally: - ⌃C with text selected in Discord → drops via ax-read (path=ax) - ⌃C with image in clipboard, text selected → text wins (was: image won) - ⌃C with nothing selected anywhere → screencapture crosshair fires - Esc during screencapture → "Cancelled — nothing dropped" Supersedes previous proxy-via-web-client approach on this branch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…onichi#904) * fix(bridges): team/other codex output via staging path + atomic move The current team/other tier system-instructions inject: codex exec --sandbox read-only -o {RESULTS_DIR}/task-{id}.txt -- ... …and tell the agent: "if codex exits non-zero OR the output file does not appear after the command finishes, treat as a failure and apply the fallback rule." That check is racy. Sequence observed today: 1. Codex completes, writes results/task-{id}.txt (exit 0). 2. Bridge polls results/, picks the file up, delivers to channel, moves to results/archive/. 3. Agent's post-exec `ls results/task-{id}.txt` happens AFTER step 2, sees "no such file," writes the 'Sandbox unavailable; refusing non-owner task.' fallback to results/task-{id}.txt. 4. Bridge delivers that second message → user receives both the real codex reply AND the spurious fallback. Caught it after Lucy's PR-skill design question (full codex review delivered, then a "Sandbox unavailable" follow-up at ~10s) and MacBook's PR sonichi#902 review ping (same pattern, real review at 19:53 then fallback ~10s later in #bot2bot-trio). Fix: rewrite both team-tier and other-tier system-instructions to use a two-stage flow: Stage 1: codex exec ... -o /tmp/sutando-codex-staging-{id}.txt -- ... Stage 2: if codex exit 0 AND staging non-empty: mv /tmp/sutando-codex-staging-{id}.txt {RESULTS_DIR}/task-{id}.txt else: write 'Sandbox unavailable; refusing non-owner task.' to {RESULTS_DIR}/task-{id}.txt directly. The bridge poller only ever sees the final file (post-mv) — no in-flight half-written results, no phantom-missing race. The two existing failure modes (codex non-zero exit, codex produced no output) collapse into one "empty/missing staging file" check evaluated BEFORE the bridge ever sees anything in results/. No bridge-side code changes; the poller and archive logic are unchanged. The only diff is the system-instructions template injected into team/other tier task files. * fix(bridges): same-fs staging path for guaranteed atomic mv Per qingyun-sutando review note sonichi#2 on sonichi#904: staging in /tmp/ + mv into {RESULTS_DIR}/ is only atomic when both share a filesystem. Today both live on the system disk so this holds. Future-proof if anyone moves $SUTANDO_WORKSPACE to an external volume: stage to {RESULTS_DIR}/.codex-staging-{id}.txt instead. Same-filesystem mv is guaranteed atomic regardless of workspace mount point. Leading dot keeps the staging file out of the results-poll glob (bridge filters by 'task-' / 'proactive-' prefix; .codex-staging-* doesn't match either). --------- Co-authored-by: Qingyun Wu <qingyun0327@gmail.com>
sonichi
added a commit
that referenced
this pull request
Sep 10, 2026
…sk (#4140) * feat(health-check): warn when a skills driver runs code older than disk `live-tree-drift` covers the repo side -- running daemons boot from whatever is on disk there. Nothing covered the SKILLS side, and the failure is quieter: a skill pulled while its driver is running never reaches that driver, because the process froze its code at launch. No probe could see it; the only evidence is the `v=<sha>` stamp the driver writes into its own log, compared by hand. Measured 2026-09-10 on Chis-Mac-mini: three pulls in one day each left the content driver executing superseded code, including one where a merged fix (#902, "the recovery delivery obeys the hold") sat on disk and ran nowhere. Each was caught only because someone happened to compare the stamp. The probe reads the newest `v=…@<sha>` from state/content-driver.log and compares it with the skills checkout HEAD. Absent log, absent checkout, unreadable stamp and unparsed HEAD all return ok, never warn: a driver that never ran has no drift, and a broken probe must not manufacture one. Both arms exercised against the live workspace rather than a fixture: matching stamp -> ok "running 4acf765, matches skills HEAD" planted stale -> warn "running 0000000 but skills HEAD is 4acf765" Stand: Echo Act IV Mini * test(health-check): exercise the skills-driver drift probe, and make it testable The probe shipped with no test: diff coverage measured 45% on the change and CI refused it. Covering it required making the workspace injectable, which also removed a `repo_root` parameter the body never read -- the reason the function could not be driven from a fixture in the first place. Ten arms, including the two that source-reading cannot establish: - the NEWEST stamp decides, so an older matching stamp cannot mask a newer mismatching one (a `stamps[0]` read reports ok on a drifted driver) - inverting `running == head` in the source must break the healthy case, so the comparison is exercised rather than merely present One behavioural fix the arms surfaced: an unreadable skills HEAD reported "no stamp recorded yet", naming the wrong cause. It now names its own. Coverage of the added lines measured with the repo's own rcfile: no line in the probe's range remains missing, with 306 missing ranges elsewhere in the file as the live-instrument control. Stand: Echo Act IV Mini * fix(health-check): the drift probe uses the repo's macOS-safe git resolver Review finding (qingyun-wu, confirmed by yixuan-ag2 who withdrew an approval over it): the probe invoked a bare `git`. On a clean macOS host that resolves to the developer-tools stub and raises the install dialog on EVERY health pass, and neither the `except Exception` nor `timeout=10` can suppress a dialog. The probe now builds its argv with `git_argv` and degrades on `GitUnavailable`. This was the local convention of the function's own neighbours, not a repo-wide one I could reasonably have missed: `health-check.py` already calls the helper at fifteen sites, and the new probe was the single outlier. Two controls, because the existing arms could not have caught this — they pin what the function DECIDES and the defect was in what it INVOKES: j) spies the subprocess boundary and asserts no built argv starts with a bare "git". Verified to FAIL when the call is reverted to bare git, and to pass restored. j2) `git_argv` raising GitUnavailable -> ok degrade naming that cause. Stand: Echo Act IV Mini
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
Three changes on top of PR #891's ax-read subprocess groundwork to make ⌃C reliable:
AXIsProcessTrustedWithOptions(prompt:true)at startup — re-binds TCC Accessibility to the current binary after codesign identity changes. Without this, the prior grant orphans silently and AX queries fail with no re-prompt. The 2026-05-19 incident: I re-signed Sutando.app (ad-hoc → "Sutando Dev" cert) and AX queries started returning empty for every app (Discord and native TextEdit) despite the System Settings toggle still appearing on.AXIsProcessTrusted=falseat startup proved it.changeCountguard → clipboard PNG → clipboard TIFF → screen-region capture.screencapture -i -clast-resort fallback. When nothing matched anywhere, instead of "Nothing selected" macOS now draws the crosshair so the user can drag-select a region to drop. Esc cancels cleanly. Runs on a backgroundDispatchQueueso the main run loop stays responsive while the user drags.screencaptureInFlightinstance bool guards against stacked crosshairs from rapid ⌃C.Why subprocess
invokeAxReadworks from the menu-bar agent nowThe earlier failure mode in PR #891 (subprocess AX returning empty when spawned from
Sutando.appLSUIElement=YES process context) was a TCC attribution issue, not a fundamental macOS quirk. The OS attributes the child's AX queries up the responsible-process chain toSutando.app. When the bundle's TCC Accessibility entry didn't match the running binary's signature (cdhash mismatch from the prior ad-hoc sign), the AX queries silently returned empty for every target app — Discord, TextEdit, all of them. The startupAXIsProcessTrustedWithOptionscall now forces a re-bind on first launch after any signature change, so the spawnedax-readinherits a current grant. Verified locally: withAXIsProcessTrusted=true, the subprocess returnspath=axfor Discord selections.Out-of-band: codesign DR for cdhash stability
The startup AX-trust prompt only helps if the grant persists across rebuilds. For self-signed certs that aren't in the system trust store, TCC falls back to cdhash matching — every rebuild orphans the grant. Fix is to embed a permissive designated requirement at sign time:
Then TCC stores the requirement at grant time and future rebuilds (same identifier, different cdhash) keep working.
src/startup.shnow detects a "Sutando Dev" cert in the keychain and applies this requirement automatically; without that cert it falls back to ad-hoc (legacy behavior). Verified locally: rebuilt with different cdhash,AXIsProcessTrusted=trueon first run.A bare identifier-only DR is permissive — any binary signed with the same identifier inherits the grant. Stronger would be
identifier + anchor apple generic + cert leaf[Y]. The permissive form is fine for personal-use Sutando but flagged here in case a future user installs an Apple Dev cert and wants tighter binding.Test plan
path=ax)🤖 Generated with Claude Code