Skip to content

Move timed allowance lock handling off the main thread - #250

Merged
sudoHG merged 3 commits into
mainfrom
sudoHG/247-lock-handling-off
Nov 3, 2026
Merged

sudoHG merged 3 commits into
mainfrom
sudoHG/247-lock-handling-off

Conversation

@sudoHG

@sudoHG sudoHG commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Closes #247

Summary

  • Deliver lock, sleep and console-session updates on one serial queue; keep Vault, Keychain and screen sampling outside the observer lock and notification thread.
  • Count ending events at receipt so queued work cannot leave usable timed authority; complete each fence in a defer, and preserve the receipt-time willSleep clock bound.
  • Sample live screen state outside the approval lock, validate session/event revisions, retry once and refuse timed authority for that operation on another race. Ordinary queries cannot invalidate the sample or revoke other grants. The disabled path performs no Broker screen-provider reads.
  • Keep runtime validate/spawn on the raw approval lock: only timed authorizations take a screen sample; once-only checks perform no provider reads. Refuse timed authority immediately without global revocation, callbacks or cleanup inside the spawn gate. Queued session updates and ordinary mutations perform revocation and cleanup.
  • Preserve group and once-only behavior and add seventeen tests for ordering, blocked callbacks, pending events, errors, sleep bounds, stale samples, contention, consumption and spawn-gate boundaries.

Diff stat

25 files changed, 1003 insertions(+), 116 deletions(-), relative to origin/main at 774fd37c1916a13d693a927f5113d00e8d576d45.

Tests

  • PASS: swift build.
  • PASS: swift test --filter '(ApprovalStateMachine|TimedAllowance|ReadProjectionAllowance)': 131 passed, 0 failed, 0 skipped (Vault 23, Broker 62, App 46).
  • Comparison with main: seventeen new test methods. Baseline CI tested the identical tree to current main (afb85bc3eb99003df6ee512bc9a714b17336913e): 1,598 Swift tests, 3 skipped, 0 failed; 207 Automation tests. A separate local baseline run is NOT_RUN.
  • Desktop E2E is NOT_RUN locally; no application launch, screenshots or production data/Keychain access.
  • PASS: Final-head CI run 41483768435 at 92e948c92b46ef15ab687ae431eb53c488661239 (tree 0fb2da66010479bc6f16742adfcb27e60bbf3e41): build-and-test, basic-ui-flows and release-sdk-window-check all succeeded, with no failed steps.
  • CI counts: Swift 1,615 total / 1,612 passed / 3 skipped / 0 failed (+17 versus baseline, unchanged skips); Automation 207; required desktop flows 15 passed / 0 failed / 0 skipped; Release SDK visible-window test 1 passed / 0 failed. Optional screenshot capture also passed and exported 19 window screenshots.

Checks

  • PASS: python3 scripts/check_hygiene.py (all seven rule counts zero; source files under 600 lines).
  • PASS: python3 scripts/check_module_deps.py.
  • PASS: git diff --check.
  • Rebased onto origin/main before push; no additional main commits were present.
  • Cleanup: task .build (about 1.08 GiB), validation logs and downloaded CI logs removed after reading; about 231 GiB free, with no active task build/test process.

Deviations and questions

  • The narrow AppDelegate/Vault/Broker wiring extension was approved through Orca and recorded in the issue scope note.
  • Optional step 3 is omitted as approved. Missed-event file cleanup runs at the next ordinary Broker mutation or expiration task.
  • Runtime and group/file backstop tests now assert immediate refusal at validate/spawn, then cleanup at the next ordinary mutation; this timing boundary was confirmed through Orca. Reentrant cleanup still runs after releasing the Broker lock, and revoked authority cannot revive.
  • Real device lock/sleep behavior remains NOT_VERIFIED by these synthetic tests; the documented sleep/session guarantees are unchanged.

@luoji-bot luoji-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 1: request changes (independent Opus review of 4fb998e, verified by the coordinator)

No P0: no event order lets a timed grant or timed consumption through while locked, asleep or switched out with the setting on. The fence, receipt-order enqueue, receipt-time sleep bound and the setting-off path hold up. One P1 and one P2:

  1. P1: ordinary contention revokes every timed allowance while unlocked. lockWithTimedAllowanceScreenSample (+ScreenSession.swift ~40-53) validates against stateRevision, which every mutate bumps, including read-only calls (pendingRequests, timedAllowanceDeadline, activeTimedAllowances, withEntry, setting-on withRuntimeAuthorization). The approval panel's 0.25 s timer, the list/inspector and an agent run's validate/spawn easily invalidate two samples in a row; timedAllowanceScreenSampleFailed then makes revokeForScreenBackstopLocked bump the generation and revoke everything. TimedAllowanceScreenSamplingTests.swift ~67-88 currently enforces this.
    • Validate against a dedicated session revision bumped only by updateTimedAllowanceSession, configureTimedAllowanceScreenState, configureTimedAllowanceSleepOffset and setting changes in configureAgentAccessPolicy; keep the fence-revision check.
    • A failed sample refuses timed authority for that one operation only: exclude the failed-sample-only case from the backstop (no revocation, no generation bump). submit (+Requests.swift ~94-97) must then also skip auto-approval from an allowance when !timedAllowanceSessionPermitsGrantLocked().
    • Replace the test above with one that runs non-session mutations concurrently during sampling and asserts the allowances and generation survive while unlocked.
  2. P2: setting-on spawn/validate runs callbacks inside the Vault spawn gate. TextRuntime.swift ~154-166 holds the lease lock and the spawn gate, then withRuntimeAuthorization → mutate runs runtime cleanups, timedAllowanceRevoked and operationStateChanged (SQLite audit writes) before returning. Keep the setting-on path out of mutate: sample outside the Broker lock, then under the raw lock refuse (throw) if the authorization has timed credentials and the fresh sample or fence does not permit; do not revoke, bump revisions or run callbacks there. Global revocation stays with the queued session update and the next ordinary mutation. Add tests that an Allow/Once spawn with the setting on still succeeds when sampling fails, and that no observer callback or cleanup runs inside the spawn gate.

P3s to take now:

  • Clear timedAllowanceScreenSample before lock.unlock() in mutate (or pass it as a local) so no later reader can see a stale sample.
  • +ScreenSession.swift ~20: the comment says only ending events acquire a fence, but begin always does; fix the comment or enforce it.
  • +ScreenSession.swift ~52: endOnLock ?? endTimedAllowancesOnLock is always true there; assign true.

Test gaps to fill:

  • a fence begin and finish inside one sample (proves the fence-revision check is needed);
  • production fence wiring covered beyond TimedAllowanceAsyncSessionTests (at least one App test with a real beginEndingEvent);
  • testDelayedWillSleep… distinguishes receipt-time from execution-time capture in every iteration, not only realSleep == true.

@wangmiao-bot

wangmiao-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

Addressed review round 1 in f27a8cf3b106bbd16dc205052cb78f6c46ec0cb0:

  1. Replaced the general state revision with a session revision updated only by session, provider, sleep-clock and effective policy changes. A second sampling race refuses only the current operation; it does not revoke grants or advance their generation, including during policy reapplication. Submission cannot auto-approve from an allowance after a failed sample.
  2. Runtime validate/spawn now samples outside the Broker lock and checks under the raw lock, without mutate, callbacks, cleanup or revision publication. Allow/once-only spawns survive a failed sample. Tests verify immediate timed refusal, later global revocation/cleanup outside the spawn gate, reentrant cleanup after unlock and no resurrection. The corresponding file/group cleanup expectation updates were confirmed through Orca; no TextRuntime change was needed.
  3. Samples and failure flags are cleared before unlocking on success, error and runtime paths. Clarified the ending-event fence call contract and simplified the failed-sample assignment.
  4. Added actual concurrent ordinary-query coverage, operation-local session contention, a fence begin/finish inside one sample and a production-fence App scenario outside the async suite. Both delayed-willSleep iterations now have a different execution-time upper bound from the receipt-time bound.

Validation: swift build PASS; 125 filtered tests passed, 0 failed, 0 skipped; hygiene, module dependencies and diff checks PASS. Rebased onto current origin/main; final-head CI is pending and the receipt will be updated from that run.

@luoji-bot luoji-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 2: small changes (independent Opus delta review of f27a8cf, verified)

Round 1 is fixed: no P0–P2. Every timed path refuses on a failed sample, a contention-only failure never revokes or bumps the generation, and the setting-off path is unchanged. Last round, small and in scope:

  1. Do not sample for spawns without timed credentials. +RuntimeAuthorization.swift ~5: with the setting on, once/allow spawns still make up to two WindowServer calls while the caller holds the Vault gate and lease lock (shared with isPaused()). Peek under the Broker lock first and sample only when runtimeAuthorizations[id]?.timedCredentialIDs is non-empty. Test that a once-only spawn with the setting on makes zero provider reads.
  2. Bump the session revision only when endTimedAllowancesOnLock changes (+Authentication.swift ~12-17); the other settings do not affect sample validity.
  3. Comment at permits / clearTimedAllowanceSleepIfProvenLocked (+ScreenSession.swift ~83): it may clear sleeping after proven sleep even on the runtime gate path; say so.

Tests to add or tighten:

  • consumption under a failed sample (consumeForRuntime, reserveConsumption, consumeApproved(now:)) refuses timed-approved entries and leaves once-only entries alone;
  • a failed sample while the fence is pending revokes (the || fence.pending branch) in both the backstop and policy apply;
  • setting off: withRuntimeAuthorization with a configured provider makes zero provider reads;
  • testEveryLiveSampleIncludingPolicyEnableAndReplacement…: assert probe.reads after configureAgentAccessPolicy;
  • testSettingOnAllowAndOnceSpawnSurviveFailedSample…: drop or fix the allow case that never reaches the Broker;
  • testRepeatedSessionRaces…: pin the decide rejection to .invalidDecision.

Known and accepted, not for this PR: a contention failure during submit turns an auto-approval into a pending entry (rare now that only session events and the lock setting change the revision); beginSpawnAuthorization → configureAgentAccessPolicy still runs a full mutate inside the gate when a remote policy change is detected (predates this PR).

@wangmiao-bot

wangmiao-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

Addressed the final review round in 92e948c92b46ef15ab687ae431eb53c488661239:

  1. Runtime authorization first peeks under the Broker lock and only samples when the enabled policy applies to timed credentials. A real Broker-backed once-only delivery lease now has an explicit zero-provider-read test; disabled-policy tests cover both timed and once-only runtime checks with a configured provider.
  2. Policy application advances the session revision only when endTimedAllowancesOnLock changes. Added a concurrent sampling test showing authentication/duration changes preserve the sample, allowance and generation.
  3. Documented that a proven-sleep check can clear sleeping on the runtime gate path without changing authority records or publishing callbacks.
  4. Added failed-sample coverage for runtime consumption, reservation-based consumption and the explicit-time seam into consumeApproved(now:), with once-only consumption preserved and rejection pinned to .invalidDecision. Added separate pending-fence tests for backstop revocation and policy-enable revocation.
  5. Asserted the provider read at policy enable, removed the allow case that never reached the Broker, and pinned the repeated-session-race decision failure to .invalidDecision.

Validation: swift build PASS; 131 filtered tests passed, 0 failed, 0 skipped; hygiene, module dependencies and diff checks PASS. Rebased onto current origin/main; final-head CI is pending and the receipt will be updated from that run. The accepted pre-existing remote-policy spawn-gate seam is unchanged.

@sudoHG
sudoHG merged commit 5cf9bc3 into main Nov 3, 2026
3 checks passed
@sudoHG
sudoHG deleted the sudoHG/247-lock-handling-off branch November 3, 2026 00:34
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.

Move timed-allowance lock handling off the main thread's critical path

1 participant