# Apple DTS / Feedback — NEURLFilter `serverSetupIncomplete` on a development-signed build

**Zweck:** Einreichfertiger Bericht für (1) einen Feedback-Assistant-Bug-Report und
(2) einen Apple Developer Technical Support Incident (TSI). Stand: 2026-05-21.
Vor dem Einreichen die Platzhalter `pir.staging.rebreak.org` und `<user token>` durch die echten
Werte ersetzen. Einreich-Anleitung siehe Abschnitt „How to submit" am Ende.

---

## Summary

`NEURLFilterManager.saveToPreferences()` fails on a **development-signed** build
installed directly via Xcode (`expo run:ios --device`). `NEURLFilterManager.status`
never reaches `.running`; `lastDisconnectError` is `.serverSetupIncomplete`.

The device performs the PIR use-case registration successfully, but then issues a
`POST /config` to our PIR server **without a Privacy Pass token** and treats the
resulting `401` as fatal. The device **never** performs Privacy Pass token issuance
(no request to the issuer directory, `/token-key-for-user-token`, or `/issue` ever
reaches our server). The underlying CipherML error is logged redacted
(`com.apple.CipherML Code=1800 "Error details were logged and redacted."`).

We would like to know **why the device does not acquire a Privacy Pass token** before
calling `/config`, and what "server setup" the framework considers incomplete.

## Environment

- Device: iPhone, iOS 26.5
- Build: development-signed, installed directly from Xcode toolchain (`expo run:ios --device`)
  - Verified: `get-task-allow = true`, `aps-environment = development`
  - Provisioning profile: "iOS Team Provisioning Profile" (Development, ProvisionedDevices present)
  - Entitlements: `com.apple.developer.networking.networkextension` includes `url-filter-provider`;
    `com.apple.developer.family-controls = true`
- App bundle id: `org.rebreak.app`; control-provider extension: `org.rebreak.app.URLFilterExtension`
- PIR server: self-hosted, `apple/pir-service-example` (`PIRService`), public HTTPS endpoint

## Configuration used

```swift
try manager.setConfiguration(
    pirServerURL: URL(string: "https://pir.staging.rebreak.org")!,            // no trailing slash
    pirPrivacyPassIssuerURL: URL(string: "https://pir.staging.rebreak.org")!,  // same host serves both
    pirAuthenticationToken: "<user token>",
    controlProviderBundleIdentifier: "org.rebreak.app.URLFilterExtension")
manager.localizedDescription = "ReBreak URL Filter"   // added for WWDC-sample parity
manager.prefilterFetchInterval = 86400                // added for WWDC-sample parity
manager.isEnabled = true
manager.shouldFailClosed = true
try await manager.saveToPreferences()
```

The configuration the system actually received (from `neagent` logs) is correct:

```
urlFilter = {
    Enabled = YES
    FailClosed = YES
    AppBundleIdentifier = org.rebreak.app
    ControlProviderBundleIdentifier = org.rebreak.app.URLFilterExtension
    pirServerURL = https://pir.staging.rebreak.org
    pirPrivacyPassIssuerURL = https://pir.staging.rebreak.org
    AuthenticationToken = <user token>
    pirPrivacyProxyFailOpen = NO
    pirSkipRegistration = NO
}
```

## What works (verified)

- The control-provider extension launches; `fetchPrefilter()` returns a valid
  `NEURLFilterPrefilter` (bloom filter loads).
- `url-filter-provider` entitlement is validated OK by `neagent`.
- PIR use-case **registration succeeds**: `NEPIRChecker … "completed registration
  with PIR for Group org.rebreak.app usecase org.rebreak.app.url.filtering"`.
- Server side independently verified healthy: the issuer directory is served, and
  `GET /token-key-for-user-token` with `Authorization: Bearer <user token>` returns
  HTTP 200 with a valid SPKI public key.

## The failure (device logs, `com.apple.cipherml` subsystem un-redacted via NE debug profile)

```
ciphermld  Request to fetchConfigs has started for useCases ['org.rebreak.app.url.filtering']
ciphermld  error  Failed to fetch configs. URL: https://pir.staging.rebreak.org/config  Status Code: 401
ciphermld  error  queryStatus(for:options:) threw an error:
                   CipherML.CipherMLError.serverError("{\"error\":{\"message\":\"No private token\"}}")
neagent    requestStatusForClientConfig… XPC complete, error: com.apple.CipherML Code=1800
                   "Error details were logged and redacted."
neagent    NEPIRChecker … PIR status returned error: com.apple.CipherML Code=1100
                   "Unable to query status due to errors…" (NSUnderlyingError Code=1800)
neagent    NEAgentURLFilterExtension startURLFilter … Failed to startFilter:
                   NEMembershipCheckerErrorDomain Code=3
→ NEURLFilterManager.lastDisconnectError = .serverSetupIncomplete
```

Our server's access log confirms: the device only ever sends `POST /config`
(User-Agent `com.apple.ciphermld/1.0 iOS/26.5`), always returning `401 "No private
token"`. Over many hours of retries we observe **zero** requests to
`/.well-known/private-token-issuer-directory`, `/token-key-for-user-token`, or
`/issue` from the device. The device never attempts Privacy Pass token issuance.

The whole `saveToPreferences()` → failure happens in well under a second.

## What we have ruled out

- **Server**: registration succeeds; `/token-key-for-user-token` returns 200 for the
  configured user token; issuer directory served correctly.
- **App configuration**: the `setConfiguration` values arrive intact at `neagent`.
- **Signing**: confirmed a genuine Development build (`get-task-allow = true`,
  Development provisioning profile) installed directly via Xcode — per the
  documentation, such builds should not require the Oblivious HTTP private relay.
- **URL formatting**: `pirServerURL` / `pirPrivacyPassIssuerURL` have no trailing
  slash, no custom port, HTTPS, host-only (no path).

## Additional diagnostic tests (2026-05-21)

- **`shouldFailClosed = false`**: `lastDisconnectError` changes from
  `.serverSetupIncomplete` to `.unknown`, and the device then makes **zero** PIR
  network requests at all — `saveToPreferences()` rejects the configuration as
  `.configurationInvalid` before any network activity. With `shouldFailClosed = true`
  the device at least sends the tokenless `POST /config`.
- **Spec-compliant user token**: replacing the user token with a 40-character
  base64-alphabet string (no hyphens, decodes cleanly) — server accepts it
  (`/token-key-for-user-token` → 200) — changed nothing on the device side.

- **WWDC2025-sample parity**: additionally setting `manager.localizedDescription`
  and `manager.prefilterFetchInterval` (so the configuration matches Apple's
  WWDC2025 sample code exactly) — no change. `saveToPreferences()` still fails with
  `.configurationInvalid`; `lastDisconnectError` is now `nil` (the rejection is now
  synchronous, before any server connection attempt).

Server, app configuration, and code signing are therefore all confirmed not to be
the cause. Every public `NEURLFilterManager` parameter is set and matches Apple's
own WWDC2025 sample, yet `saveToPreferences()` rejects the configuration as
`.configurationInvalid`.

## Questions for Apple

1. Why does the device call `POST /config` **without** first performing Privacy Pass
   token issuance (issuer directory → `/token-key-for-user-token` → `/issue`)?
2. What does `serverSetupIncomplete` / `CipherML Code 1800` concretely indicate here,
   given registration succeeded and `/token-key-for-user-token` works? The detailed
   error is redacted even with the Network Extension logging profile installed —
   which profile un-redacts `com.apple.CipherML` errors?
3. For a **development build installed via Xcode**, is a provisioned privacy proxy /
   OHTTP onboarding nonetheless required for Privacy Pass token issuance? The
   `pirPrivacyProxyFailOpen = NO` flag in the resolved config suggests the privacy
   proxy path is mandatory — is that expected for dev builds?
4. Is there a token-pool warm-up delay (background token fetch) that must elapse
   before `saveToPreferences()` can succeed, and if so how should the app handle it?

## Reproduction

Standard `apple/pir-service-example` `PIRService` as the PIR + Privacy Pass issuer,
a development-signed app with an `NEURLFilterControlProvider` extension, and the
`setConfiguration` call shown above. Happy to provide a sysdiagnose and full
`neagent`/`ciphermld` logs.

---

## How to submit (internal note — not part of the report text)

File in this order:

1. **Feedback Assistant** (`feedbackassistant.apple.com`) — file as a bug against
   *NetworkExtension / NEURLFilter*. Attach a **sysdiagnose** taken right after
   reproducing, plus the full `neagent` + `com.apple.cipherml` Console export.
   Note the resulting **FB number**.
2. **Developer Technical Support Incident** (developer.apple.com → Account →
   Support → *Technical Support* / „Request Technical Support") — paste this
   report, reference the **FB number** from step 1, and ask the four
   „Questions for Apple". A DTS engineer replies directly (effectively the
   written line to Apple — there is no plain support-email channel for this).
   Costs one Technical Support Incident (2 included per membership year).

**Attach:** sysdiagnose (`.tar.gz`), the full `neagent`/`ciphermld` log export,
and — if asked — a minimal sample project. Replace `pir.staging.rebreak.org` and
`<user token>` with the real values first.
