Skip to content

Embed provisioning profiles so extensions actually work#107

Merged
iret77 merged 15 commits into
mainfrom
fix/provisioning-profiles
May 13, 2026
Merged

Embed provisioning profiles so extensions actually work#107
iret77 merged 15 commits into
mainfrom
fix/provisioning-profiles

Conversation

@iret77

@iret77 iret77 commented Apr 27, 2026

Copy link
Copy Markdown
Owner

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 .entitlements files listing app-group, HostBundleIdentifier, etc. The CI's own verify step printed the warnings — but only as echo, not exit 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:

  • Host couldn't open App Group container (containerURL(forSecurityApplicationGroupIdentifier:) → nil)
  • FinderSync wasn't registered as owned by ai.clawsy → never appeared in Finder context menu
  • Share Extension couldn't write pending_share.json for handoff to host

Apple Portal setup (done via web UI)

  • App Group group.ai.openclaw.clawsy (matching SharedConfig.appGroup)
  • 3 App IDs: ai.clawsy, ai.clawsy.ShareExtension, ai.clawsy.FinderSync — each with App Group capability linked to the group above
  • 3 Mac Direct Distribution provisioning profiles (one per bundle ID), 18-year validity, signed with the existing Developer ID Application cert

Code changes

  • .github/workflows/build.yml — new "Decode Provisioning Profiles" step writes the three profile secrets to ./profiles/; passes PROFILES_DIR to build.sh
  • build.sh
    • new step embeds each profile as Contents/embedded.provisionprofile in the matching bundle before re-signing
    • entitlement verification (HostBundleIdentifier, application-groups on 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 for MARKETING_VERSION + CURRENT_PROJECT_VERSION. Without them extension Info.plists expanded $(CURRENT_PROJECT_VERSION) to null, triggering warning: 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 of Clawsy Direct.mobileprovision
  • PROFILE_SHARE_BASE64 — base64 of Clawsy Share Direct.mobileprovision
  • PROFILE_FINDERSYNC_BASE64 — base64 of Clawsy FinderSync Direct.mobileprovision

Profiles also stored locally at /Users/customer/Projekte/Ressourcen/Apple Dev/profiles/*.mobileprovision (mode 0600) for future rotations.

Test plan

  • CI build green; new entitlement-verify step does not fire "❌ MISSING"
  • codesign -d --entitlements :- on each .appex shows the expected entitlements (app-group, FinderSync HostBundleIdentifier where applicable)
  • Notarization still passes
  • After install: FinderSync extension shows up in System Settings → Privacy & Security → Extensions → Added to Finder
  • After install + activation: right-click in ~/Documents/Clawsy shows Clawsy menu items
  • After install + activation: "Share" menu in any app shows Clawsy as a target

Christian Westerkamp and others added 15 commits April 27, 2026 16:36
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.
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.
@iret77 iret77 merged commit 8d5066c into main May 13, 2026
1 check passed
@iret77 iret77 deleted the fix/provisioning-profiles branch May 13, 2026 11:07
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.

1 participant