Add Deno and macOS Keychain smoke coverage - #847
Conversation
Autoreview found regressions in the KeychainManager ownership rewrite for fix-access. The ownership path read and deleted items from an explicit keychain, then recreated them in the default keychain. It also rejected valid service-only refs, could delete matching internet-password items while rewriting a generic password, and used a lossy security CLI fallback that could strip trailing newlines from secrets. Batch fix-access also unlocked all keychains before processing any item, so one stale or unavailable keychain could abort the whole batch instead of failing only that ref. Fix this by preserving keychainName on generic-password writes, resolving account-less refs only when unambiguous, reading exact secret data through SecItemCopyMatching for ownership, deleting only the targeted generic-password item, falling back to legacy ACL handling for unsupported cases, and moving batch unlock into per-item handling. Validation: bun run lint:fix; bun run --filter varlock build:binary; autoreview --mode local --engine codex reported no accepted/actionable findings.
Add Deno execution support to the shared smoke-test runner so the existing smoke tests can be reused under deno run -A instead of maintaining a separate deno-only test file. Also teach varlock completion generation how to emit shell completion scripts when invoked through Deno, and route keychain-set through the shared runner so it participates in the Deno smoke path.
When takeOwnership is scoped to a named keychain, ownership reads and deletes must not fall back to the default keychain search list if that name cannot be resolved. A typo in --keychain could otherwise read or delete a matching item from another keychain before failing during recreation.\n\nResolve the requested keychain explicitly for both the ownership read and delete paths, and throw keychainNotFound when resolution fails.
takeOwnership has to delete and recreate the generic password item so VarlockEnclave becomes the owner. If recreation failed after the delete, fix-access could permanently lose the user's secret.\n\nCatch recreation failures, immediately attempt to write the saved service/account/value back, and return a specific ownershipTransferFailed error that reports whether the secret value was restored. This keeps fix-access from silently turning a repair attempt into destructive migration.
Change takeOwnership to avoid deleting the original item until a replacement has been created and verified. The flow now creates a temporary generic-password item with the same secret, reads it back to verify it is usable, deletes the original item only after that check succeeds, renames the temporary item to the original service/account, then verifies the final item.\n\nIf the swap fails after the original delete, keep the previous recovery behavior: restore the saved secret value at the original service/account when possible, and report whether that recovery succeeded.
Restore fix-access to the legacy ACL-editing behavior so it preserves existing per-app Keychain access on existing items. Move the delete-and-recreate ownership rewrite behind a separate keychain take-ownership command. The new command is explicit about the ACL-reset tradeoff and still preserves named-keychain targeting when it recreates generic-password items. Validation: bun run lint:fix; bun run --filter varlock build:binary; bun run --filter varlock typecheck; autoreview --mode local --engine codex reported no accepted/actionable findings.
Add a macOS Keychain smoke test for the explicit take-ownership path and include it in the keychain smoke scripts. Validation: bun run lint:fix; bun run --filter varlock build:binary; VARLOCK_RUN_KEYCHAIN_SMOKE=1 bunx vitest run tests/keychain-ownership.test.ts from smoke-tests.
Before running keychain fix-access, assert that a restricted smoke-test item is not readable yet. Add timeout support to the smoke-test runner so the pre-fix read cannot hang indefinitely on Keychain authorization. Validation: bun run lint:fix; bun run --filter varlock build:binary; VARLOCK_RUN_KEYCHAIN_SMOKE=1 bunx vitest run tests/keychain-fix.test.ts from smoke-tests.
This reverts commit 8b1c78d.
Add a useFallback option to keychain() reads. It defaults to true to preserve the existing /usr/bin/security fallback, but useFallback=false now requires the Swift daemon to read through Apple's framework APIs directly. Use the option in the keychain fix-access smoke fixture so the test proves fix-access grants VarlockEnclave direct framework access instead of succeeding via the security CLI fallback. Validation: bun run lint:fix; bun run --filter varlock typecheck; bun run --filter varlock build:binary; VARLOCK_RUN_KEYCHAIN_SMOKE=1 bunx vitest run tests/keychain-fix.test.ts from smoke-tests; autoreview --mode local --engine codex.
Kill and clear Varlock daemon state before and after every macOS Keychain smoke test so stale daemon access state cannot affect prompt behavior or test results. Validation: bun run lint:fix; bun run --filter varlock build:binary; VARLOCK_RUN_KEYCHAIN_SMOKE=1 bunx vitest run tests/keychain-fix.test.ts from smoke-tests.
Replace the separate post-test keychain daemon cleanup helper with resetVarlockDaemon in both beforeEach and afterEach. The important behavior is killing the daemon and clearing pid/socket state; directory removal is unnecessary. Validation: bun run lint:fix; bun run --filter varlock build:binary; VARLOCK_RUN_KEYCHAIN_SMOKE=1 bunx vitest run tests/keychain-fix.test.ts from smoke-tests.
5e42121 to
bdd1a0a
Compare
|
I'm not sure about deferring to Also I had thought that the main reason to read from existing keychain items is the case where a user is already using that item in other contexts, or for example where an IT department is managing those items for the user. In this case I dont think recreating the item will be a good idea. One thing to note - it could be that you are seeing more prompts than would actually occur since you are not signing the built binary whereas the real one will be signed. |
|
@pullfrog review this |
There was a problem hiding this comment.
ℹ️ No critical issues — one minor cleanup inline.
Reviewed changes — this PR adds Deno IPC/completion support and reworks the macOS Keychain flows (fix-access, a new non-destructive cloneToOwned, a new delete, and a /usr/bin/security read fallback), backed by gated smoke tests.
- Allow Deno clients over IPC — add
denotoPeerIdentity.swift'sallowedBinaryNames, and generate a Deno-aware completion script (getDenoCompletionExecincli-executable.ts) via the new@bomb.sh/tabdependency. - Rework
fix-accessto a native read prompt —fixAccessForRefsnow performs akeychainGet(useFallback=false)read (guiding the user to "Always Allow") instead of mutating item ACLs; thekeychain-fix-accessdaemon action andKeychainFixAccessResulttype are removed. - Add
securityCLI read fallback —KeychainManager.getItem/getItemOfClassgain auseFallbackflag; onerrSecAuthFailed/errSecInteractionNotAllowed(or other errors) they fall back togetGenericPasswordViaSecurityCLI. TheuseFallbackflag is threaded through the resolver, daemon client, and daemon dispatch, and documented in the keychain docs. - Add
keychain cloneToOwned— non-destructive command that reads a source secret and recreates it as a Varlock-owned item (optionally writing the env ref); replaces the previous destructive take-ownership approach. - Add
keychain delete— new CLI command, daemonkeychain-deleteaction,KeychainManager.deleteGenericPassword, andKeychainDeleteResulttype.setGenericPasswordalso gains an optionalkeychainName. - Add gated smoke tests + helpers — opt-in Deno (
VARLOCK_RUN_DENO_COMPAT=1) and Keychain (VARLOCK_RUN_KEYCHAIN_SMOKE=1) suites, a sharedresetVarlockDaemonhelper, stdininputsupport inrun-varlock, and new fixtures/scripts. - Docs + changesets — keychain docs updated for the new commands and
useFallback, plus threepatchbump files.
Claude Opus | 𝕏
=> The CodexBar App by Peter Steinberger does this to avoid too many access prompts when accessing things like: Anthropic OAuth Token for claude code to read usage from the token. In this example, CodexBar is not the owner of the secret too, so i figured, this fallback might be useful for us here. But I admit, getting into the weeds of keychain behavior feels like a lot of trial and error. As I implemented the fix it seemed to help first, but I later realized your point of a non-signed dev build locally.
After fixing that by signing the builds with my own apple developer key, the prompt behavior got better already.
=> yes, I agree, the copy-then-own method seems not good, so i ripped it out again. |
|
Follow-up on the Keychain behavior: I removed the For the original case—someone wants to use a secret that already exists in Keychain and is not owned by Varlock—the best flow is to choose Always Allow when macOS prompts. That adds VarlockEnclave to that item's ACL, so subsequent reads should no longer prompt. Allow Once only authorizes the current read. (you probably know this, but I wanted to conserve this here for people who might read this thread :D) I also dug further into CodexBar. Delegating reads to @theoephraim what do you think of keeping these as the two explicit options?
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — reviewed the native Keychain read revision added since the prior Pullfrog review.
- Removed the command-line fallback — deleted the
/usr/bin/securitygeneric-password fallback so secret reads consistently authorize VarlockEnclave throughSecurity.framework. - Removed fallback configuration — removed
useFallbackfrom the resolver, daemon client, daemon dispatch, documentation, changesets, and generated smoke fixture. - Clarified persistent access behavior — updated the Keychain guidance to explain how choosing Always Allow grants VarlockEnclave persistent access, while Allow Once applies only to the current read.
azure/gpt-5.6-sol | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes focused on the signed-helper requirement added since the previous Pullfrog pass.
- Added a Keychain smoke preflight: gated macOS Keychain runs now fail before test execution when the packaged
VarlockEnclave.appis missing, invalidly signed, ad-hoc signed, or lacks a stable Team ID. - Added signing remediation guidance: the failure message points local contributors to the repository's
APPLE_SIGNING_IDENTITYbuild path, which matches the native build script.
azure/gpt-5.6-sol | 𝕏

Summary
This PR improves Deno compatibility and makes the macOS Keychain access model explicit and non-destructive.
Deno
denoprocesses to connect to the Varlock IPC helperdeno run -AmacOS Keychain
/usr/bin/securityread fallbackvarlock keychain fix-accessto read each referenced item through VarlockEnclave and guide the user to choose Always Allowfix-accessfor one item or every explicitkeychain(...)reference in an env filevarlock keychain cloneToOwnedas an explicit, non-destructive way to create a Varlock-owned copyvarlock keychain deletefor generic-password itemsKeychain access behavior
For a secret that already exists in Keychain and is owned or managed outside Varlock, the normal path is to read it in place. macOS may prompt for access; choosing Always Allow adds VarlockEnclave to that item's ACL so repeated reads no longer prompt. Choosing Allow Once only authorizes the current read.
cloneToOwnedis optional. It reads the source through Security.framework, creates a separate Varlock-owned destination item, optionally writes itskeychain(...)env reference, and leaves the original item unchanged. The PR does not perform automatic ownership transfer or delete/recreate existing external items.Smoke coverage
The gated macOS suite covers
fix-access,cloneToOwned,set, andimport, including subsequent reads and daemon-state resets between tests./usr/bin/securityis used only by test setup and cleanup to create or remove fixtures, never as a production secret-read path.VARLOCK_RUN_DENO_COMPAT=1VARLOCK_RUN_KEYCHAIN_SMOKE=1