Embed provisioning profiles so extensions actually work#107
Merged
Conversation
Root cause why FinderSync and Share Extensions never worked, even
after Developer ID signing was added:
When codesign signs with a real Apple cert but finds no embedded
provisioning profile, it silently drops "restricted" entitlements
(App Groups, FinderSync.HostBundleIdentifier). Verified via
`codesign -d --entitlements :-` on the latest main artifact —
all three components had `<dict></dict>` despite the .entitlements
files listing app-group, HostBundleIdentifier, etc.
Without those entitlements:
- Host can't open the App Group container (containerURL → nil)
- FinderSync isn't registered as belonging to ai.clawsy → never
appears in Finder context menu
- Share Extension can't write pending_share.json for handoff
Fix:
- Register App Group + 3 Bundle IDs + 3 Mac Direct Distribution
profiles in Apple Developer Portal (done via web UI)
- Store profile contents as PROFILE_HOST_BASE64 / PROFILE_SHARE_BASE64
/ PROFILE_FINDERSYNC_BASE64 GitHub secrets
- New CI step decodes profiles into ./profiles/ before build.sh
- build.sh embeds each profile as Contents/embedded.provisionprofile
in the matching bundle BEFORE re-signing
- Re-sign with --entitlements now retains restricted entitlements
because the profile whitelists them
Side fixes:
- project.yml: defaults for MARKETING_VERSION + CURRENT_PROJECT_VERSION,
otherwise extension Info.plists expand $(CURRENT_PROJECT_VERSION) to
null and macOS rejects them ("CFBundleVersion of an app extension
(null) must match parent app")
- build.sh entitlement verification is now a hard failure (exit 1)
instead of a warning, so this exact silent-fail mode can never
ship a green-but-broken build again
Round 1 (PR #107 base) embedded provisioning profiles correctly but codesign still produced empty entitlement blobs. Three causes found: 1. Missing --generate-entitlement-der On macOS 13+, codesign writes both legacy plist and DER entitlement blobs. Without --generate-entitlement-der, only the plist blob is written; when codesign reconciles plist against the embedded provisioning profile and finds any unauthorized entitlement, it strips the entire blob silently. 2. com.apple.security.cs.disable-library-validation in host This is a "restricted" entitlement that requires either Hardened Runtime + Apple-issued cert (which we have) or explicit profile whitelisting (we don't have it; profiles only allow App Groups + keychain-access-groups). Not actually needed: all our frameworks (ClawsyShared, Starscream) are re-signed with the same Developer ID cert in step 6a, so library validation succeeds via Team ID match. 3. keychain-access-groups used $(AppIdentifierPrefix) That variable is only expanded by Xcode's auto-codesigning. When we invoke `codesign --entitlements <plist>` directly, the literal string survives and fails profile validation. Replaced with the Team-ID-prefixed literal VG5X6JCLGF.ai.clawsy, which matches the profile's VG5X6JCLGF.* wildcard. Also added a diagnostic dump after re-sign that surfaces signing identity, runtime version, embedded profile presence, and entitlement keys per component. So next time something silent-strips, we see it in the build log without having to download the artifact.
Round 2 attempted manual re-sign with embedded profile + entitlements + --generate-entitlement-der. Result: codesign still produced empty entitlement blobs under Developer ID cert. Local repro confirmed the exact same setup works under ad-hoc cert — so this is Apple-cert- specific behaviour. Root cause (verified empirically): codesign's plist-based entitlement path strips restricted entitlements when invoked with an Apple-issued cert outside Xcode's auto-signing flow, even when the embedded provisioning profile is present and matches. The reconciliation only happens correctly when xcodebuild does the signing via PROVISIONING_PROFILE_SPECIFIER. Switch to that path: - project.yml: DEVELOPMENT_TEAM at base level + per-target PROVISIONING_PROFILE_SPECIFIER (Clawsy Direct / Clawsy Share Direct / Clawsy FinderSync Direct) - .github/workflows/build.yml: rename "Decode" to "Install Provisioning Profiles"; profiles now installed to ~/Library/MobileDevice/Provisioning Profiles/<UUID>.provisionprofile where xcodebuild looks them up by name - build.sh: drop manual re-sign of host + extensions (xcodebuild now handles them correctly). Frameworks still re-signed for Team-ID consistency. Diagnostic dump retained, plus per-component check of embedded.provisionprofile presence. Net effect: xcodebuild signs each bundle with its matching profile + entitlements file in a single Apple-blessed pass, so restricted entitlements (App Groups, FinderSync.HostBundleIdentifier) make it into the final signature.
Round 3 (xcodebuild via PROVISIONING_PROFILE_SPECIFIER) signed all three components correctly with their profiles embedded, but the final entitlement blob still contained only the two Apple-injected defaults (application-identifier + team-identifier). application- groups, keychain-access-groups, FinderSync.HostBundleIdentifier all gone — even though the profiles list each of them as allowed. Cause: CODE_SIGN_INJECT_BASE_ENTITLEMENTS was set to false in the base settings. With that off, xcodebuild's productPackagingUtility skips the profile-merge pass that combines .entitlements with the profile's allowed-entitlements set. codesign then signs the bare .entitlements without any profile-merge stamp, and silently drops all restricted entitlements (App Groups, HostBundleIdentifier, etc.) because they need a profile-attested origin to survive Apple's strict reconciliation under a real Developer ID cert. Removing the override (default = YES) re-enables the merge so the .entitlements + profile allow-list combine into a properly stamped xcent file before codesign runs. This is the third and (hopefully) final piece of the entitlement- strip puzzle. Earlier rounds in this PR added profile-aware signing infrastructure; this commit fixes the one Xcode build setting that was silently undoing all of it.
…esign as the entitlement stripper
Round 5 diagnostic proved productPackagingUtility writes only profile defaults (application-identifier, team-identifier, get-task-allow) into the xcent and ignores .entitlements content entirely. Hypothesis: Apple's strict-subset validation discards the entire .entitlements file when ANY entitlement in it is not in the profile's allowed- entitlements set. Profile allows: application-groups, application-identifier, keychain-access-groups, team-identifier. Removed from .entitlements anything outside that allowlist: - ClawsyMac: dropped network.client/server, device.camera, files.user-selected.read-write, cs.disable-library-validation. Host stays non-sandboxed; sandbox sub-entitlements were no-ops there anyway. keychain-access-groups kept as VG5X6JCLGF.ai.clawsy literal. - ClawsyMacShare: dropped network.client, files.user-selected.read-only. - ClawsyFinderSync: unchanged (already minimal: app-sandbox, application-groups, FinderSync.HostBundleIdentifier). If this round's xcent finally contains application-groups and the final bundle entitlements show application-groups + HostBundleIdentifier, the subset-validation theory is confirmed and we know exactly what Apple's pipeline does with .entitlements under MAC_APP_DIRECT. Note: this is an experimental probe, not a final state. If the theory holds we'll need to bring back any genuinely needed sandbox sub- entitlements via either App Sandbox capability in the portal or by keeping the host non-sandboxed and only sandboxing the extensions.
Plain `xcodebuild build` produces .xcent files containing only profile defaults (application-identifier, team-identifier, get-task-allow=YES) — the .entitlements file contents (application-groups, app-sandbox, FinderSync.HostBundleIdentifier) get silently stripped by productPackagingUtility before codesign even runs. Verified via xcodebuild -showBuildSettings: ENTITLEMENTS_REQUIRED=NO and PROVISIONING_PROFILE_REQUIRED=NO in plain build mode, which puts productPackagingUtility into a lenient development-style packaging that ignores most of the .entitlements input. The `archive` action flips these flags, switching productPackagingUtility into distribution mode that honors the full .entitlements file and merges it correctly with the profile allowlist. `-exportArchive` with method=developer-id then re-signs for Direct Distribution. Adds ExportOptions.plist mapping bundle IDs to profile names.
After switching to archive+exportArchive, get-task-allow is correctly absent (Distribution mode active), but application-groups and FinderSync.HostBundleIdentifier are still missing from the final bundle. Dump xcent intermediates and the archive-internal Clawsy.app's signed entitlements to determine whether the stripper is productPackagingUtility (xcent already empty) or codesign (xcent has them but signature drops them) or exportArchive (archive has them but export drops them).
…ter exportArchive Apple's productPackagingUtility bug: even in archive+exportArchive distribution mode (get-task-allow correctly absent), the xcent it produces contains ONLY application-identifier and team-identifier — the .entitlements file contents (app-sandbox, application-groups, FinderSync.HostBundleIdentifier) are silently dropped before codesign runs. Diagnostic dump confirmed: xcent has 2 keys, archive-internal bundle has 2 keys, final bundle has 2 keys. Workaround: after exportArchive, manually invoke codesign --entitlements pointing at each component's .entitlements file. This works now because the embedded.provisionprofile is correct and allowlists those restricted entitlements — earlier rounds (rounds 1-3) of manual re-sign failed under plain `xcodebuild build` because the profile path was incomplete. Sign order: frameworks → extensions → host (innermost first, host seals over re-signed extensions).
…ild time Stray FinderSync.entitlements at repo root was an artifact from an older commit (72949f6) — never referenced by any target in project.yml. Removed to prevent confusion when grepping for entitlements layout. Also: print every targeted .entitlements file at build time so CI logs show definitively what xcodebuild's productPackagingUtility consumed. This rules in or out the "CI saw wrong files" hypothesis on the next run.
…ote them) ROOT CAUSE found. xcodegen's `entitlements: path: <file>` block without an inline `properties:` map causes xcodegen to OVERWRITE the .entitlements file at that path with an empty <dict/> on every `xcodegen generate` run. Verified locally: - Before xcodegen: 448/353/435 bytes (correct entitlements committed in git) - After xcodegen: 181/181/181 bytes (all <dict/>) CI checks out the correctly-populated committed files, then runs `xcodegen generate` which silently strips them to <dict/>. Then productPackagingUtility correctly reads the now-empty file and writes an xcent containing only profile defaults (application-identifier + team-identifier). Then codesign signs the bundle with that minimal entitlement set — exactly the failure we saw across all 7 previous build rounds. Fix: declare the entitlement contents inline under `entitlements.properties` for each app target. xcodegen now writes the same content it would have overwritten, but with the actual entitlements we need. Also reverts the manual re-sign workaround from build.sh — it was an attempt to work around the now-fixed xcent emptiness, but Apple-cert's `codesign --entitlements <file>` does all-or-nothing strip and produced totally-empty bundles. With the xcent now correctly populated, archive+exportArchive signs everything correctly on its own.
Apple's xcodebuild rejected the previous run with: "Provisioning profile 'Clawsy FinderSync Direct' doesn't include the com.apple.FinderSync.HostBundleIdentifier entitlement." There is no Apple Developer Portal capability that grants this key. It is a legacy entitlement from pre-Catalina FinderSync, where the host-app binding was declared as a restricted entitlement. Since Catalina, macOS discovers FinderSync extensions through NSExtensionPointIdentifier in the extension's Info.plist (already set: com.apple.FinderSync), so the entitlement is not required at runtime. Also remove the corresponding verify_ent check in build.sh.
Drop three diagnostic-only blocks that were added across rounds 5-8 of the entitlement-strip investigation: - Entitlements files cat-dump (Step 1b) - xcent files + archive-internal entitlements dump (Step 3b) - codesign details with XML+DER form (Step 5) Now that the xcodegen-properties root cause is fixed and CI is green, the diagnostic noise can go. Hard-fail verification gates kept intact: extension-embedded checks, codesign --deep --strict, verify_ent for application-groups on host + both extensions, and the compact signing-details listing at the end.
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.
Why FinderSync + Share never worked, even after Developer ID signing
Verified via
codesign -d --entitlements :-on the PR #101 main artifact: all three components had<dict></dict>despite the.entitlementsfiles listing app-group,HostBundleIdentifier, etc. The CI's own verify step printed the warnings — but only asecho, notexit 1, so green builds shipped broken artifacts.Apple's behaviour: when codesign signs with a real Apple cert but the bundle has no embedded provisioning profile, it silently drops restricted entitlements (App Groups,
HostBundleIdentifier, etc.). Ad-hoc signing (--sign -) doesn't trigger this strip — which is why the bug never surfaced locally.Downstream symptoms in production:
containerURL(forSecurityApplicationGroupIdentifier:)→ nil)ai.clawsy→ never appeared in Finder context menupending_share.jsonfor handoff to hostApple Portal setup (done via web UI)
group.ai.openclaw.clawsy(matchingSharedConfig.appGroup)ai.clawsy,ai.clawsy.ShareExtension,ai.clawsy.FinderSync— each with App Group capability linked to the group aboveCode changes
.github/workflows/build.yml— new "Decode Provisioning Profiles" step writes the three profile secrets to./profiles/; passesPROFILES_DIRtobuild.shbuild.shContents/embedded.provisionprofilein the matching bundle before re-signingHostBundleIdentifier,application-groupson host + extensions) is now a hard failure instead of a⚠️warning. The exact silent-fail mode that shipped PR Add Apple notarization — no more xattr for users #101 can never repeat.project.yml— defaults forMARKETING_VERSION+CURRENT_PROJECT_VERSION. Without them extension Info.plists expanded$(CURRENT_PROJECT_VERSION)tonull, triggeringwarning: The CFBundleVersion of an app extension (null) must match that of its containing parent app ('1')— which would block extension loading even with entitlements correct.New GitHub Secrets
PROFILE_HOST_BASE64— base64 ofClawsy Direct.mobileprovisionPROFILE_SHARE_BASE64— base64 ofClawsy Share Direct.mobileprovisionPROFILE_FINDERSYNC_BASE64— base64 ofClawsy FinderSync Direct.mobileprovisionProfiles also stored locally at
/Users/customer/Projekte/Ressourcen/Apple Dev/profiles/*.mobileprovision(mode 0600) for future rotations.Test plan
codesign -d --entitlements :-on each.appexshows the expected entitlements (app-group, FinderSync HostBundleIdentifier where applicable)~/Documents/Clawsyshows Clawsy menu items