From 746d809d48922a539bf83ebc8d8f67beac7d8c9b Mon Sep 17 00:00:00 2001 From: ARIA Date: Mon, 24 Aug 2026 19:04:40 +0200 Subject: [PATCH] Default push backend to ntfy; FCM opt-in with privacy warning (issue #10) - IRIS_PUSH_BACKEND now defaults to ntfy (keeps push metadata on your own infrastructure); FCM is opt-in via IRIS_PUSH_BACKEND=fcm - build_push_backend(): ntfy for empty/unknown names, FCM only on explicit 'fcm' - gateway setup: warn when FCM is chosen (metadata routed via Google's servers) - README: privacy note + dedicated push section; new docs/playstore-listing.md with the FCM/ntfy privacy note for the Play Store listing - docs: 00/02/03/08/12/16 + setup.md updated to ntfy-default wording - tests: default-backend assertion updated (86/86 pass) --- README.md | 21 ++++++++++++-- docs/00-overview.md | 2 +- docs/02-monorepo.md | 4 +-- docs/03-gateway-plugin.md | 2 +- docs/08-push.md | 11 +++++--- docs/12-toolchain.md | 7 +++-- docs/16-open-questions.md | 2 +- docs/README.md | 3 +- docs/playstore-listing.md | 40 ++++++++++++++++++++++++++ docs/setup.md | 42 +++++++++++++++------------- gateway-plugin/adapter.py | 19 +++++++++---- gateway-plugin/plugin.yaml | 2 +- gateway-plugin/push.py | 35 +++++++++++++---------- gateway-plugin/tests/test_android.py | 2 +- 14 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 docs/playstore-listing.md diff --git a/README.md b/README.md index bcd5a6d..0c9f536 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Iris pairs with your running `hermes gateway` over a private WebSocket and gives - **Native Hermes-Gateway integration** — your hermes → gateway → Iris app - **Absolute Privacy!** — everything stays on your own infrastructure + (push: ntfy by default; FCM is opt-in and routes push metadata via Google — + see [Push notifications](#push-notifications)) - **No file limit** - **No character limit** - **Full markdown support** — tables, checkmarks, bold, inline code, code blocks + syntax highlighting… @@ -36,7 +38,22 @@ hermes-agent ──> hermes gateway ──(WebSocket :8790)──> Iris app (And - The app is a first-class hermes *messaging platform*, so everything the gateway already does just works: slash commands, cron delivery, `send_message` routing, coexistence with Telegram/Discord/etc. -- Push notifications: FCM (primary) or ntfy (fallback). +- Push notifications: ntfy (default) or FCM (opt-in). + +## Push notifications + +Push wakes a backgrounded/offline device; on reconnect the app syncs the +outbox, so nothing is lost. + +- **ntfy (default)** — push metadata stays on your own infrastructure + (self-hosted ntfy recommended). This is the backend for truly private + communication. +- **FCM (opt-in, `IRIS_PUSH_BACKEND=fcm`)** — standard/reliable, but FCM push + metadata (notification title, device token) is routed through **Google's + servers**. If you want truly private communication, use ntfy instead. + +Setup: [`docs/setup.md`](docs/setup.md) §4; details: +[`docs/08-push.md`](docs/08-push.md). ## Build from source @@ -99,7 +116,7 @@ Notes: - Remote access: Tailscale/WireGuard, or a reverse proxy with CA-signed WSS (`ANDROID_WS_CERT` / `ANDROID_WS_KEY`). -Full walkthrough, push setup (FCM/ntfy), and troubleshooting: +Full walkthrough, push setup (ntfy/FCM), and troubleshooting: [`docs/setup.md`](docs/setup.md). ## Contributing diff --git a/docs/00-overview.md b/docs/00-overview.md index 54eba30..a106ae9 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -57,7 +57,7 @@ Everything in the feature checklist below. | Decision | Choice | |---|---| | Desktop app tech | **Compose Multiplatform** (shares Android code; "tweaked" for big screen) | -| Push backend | **Both** — FCM primary, ntfy fallback (`IRIS_PUSH_BACKEND`) | +| Push backend | **Both** — ntfy default, FCM optional (`IRIS_PUSH_BACKEND`) | | Media transport | **Over the WebSocket** (chunked binary frames; no extra Python deps) | | Phone default layout | **User-toggleable, single-pane default** (auto two-pane on large screens) | diff --git a/docs/02-monorepo.md b/docs/02-monorepo.md index a8e4344..68dc447 100644 --- a/docs/02-monorepo.md +++ b/docs/02-monorepo.md @@ -61,8 +61,8 @@ iris_x_hermes/ - **`media.py`** — inbound chunked upload → `cache_*_from_bytes`; outbound `media.offer`/`media.pull` chunked streaming. - **`outbox.py`** — SQLite outbox per `chat_id` + monotonic sync cursor. -- **`push.py`** — `PushBackend` interface; `FcmBackend` (httpx, FCM HTTP v1) and - `NtfyBackend` (reuses hermes ntfy publish). Selected by `IRIS_PUSH_BACKEND`. +- **`push.py`** — `PushBackend` interface; `NtfyBackend` (default) and + `FcmBackend` (httpx, FCM HTTP v1). Selected by `IRIS_PUSH_BACKEND`. - **`pairing.py`** — token generation/verification (constant-time), device registry (SQLite), QR payload. - **`search.py`** — FTS5 query bridge over the hermes session store. diff --git a/docs/03-gateway-plugin.md b/docs/03-gateway-plugin.md index cc05f9c..c4ec996 100644 --- a/docs/03-gateway-plugin.md +++ b/docs/03-gateway-plugin.md @@ -48,7 +48,7 @@ optional_env: prompt: "Allow all devices? (true/false)" password: false - name: IRIS_PUSH_BACKEND - description: "Push backend: fcm (default) or ntfy" + description: "Push backend: ntfy (default, keeps metadata off Google) or fcm" prompt: "Push backend" password: false - name: IRIS_FCM_SERVICE_ACCOUNT diff --git a/docs/08-push.md b/docs/08-push.md index 718dde7..c4e9969 100644 --- a/docs/08-push.md +++ b/docs/08-push.md @@ -1,7 +1,10 @@ # 08 — Push Notifications, Outbox & Sync The gateway can't reach a sleeping phone directly. Push goes through a cloud -relay. **Decision: FCM primary, ntfy fallback** (`IRIS_PUSH_BACKEND`). +relay. **Decision: ntfy default, FCM optional** (`IRIS_PUSH_BACKEND`). +Privacy: FCM push metadata (notification title, device token) is routed +through Google's servers — for truly private communication use ntfy +(self-hosted), which keeps everything on your own infrastructure. ## 8.1 When push fires @@ -54,9 +57,9 @@ class PushBackend(Protocol): def configured(self) -> bool: ... ``` -Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`). +Selected at adapter init by `IRIS_PUSH_BACKEND` (`ntfy` default, `fcm`). -### 8.2.1 `FcmBackend` (primary) +### 8.2.1 `FcmBackend` (optional; metadata via Google) - **FCM HTTP v1 API** via `httpx` (core dep). Auth = Firebase **service account** (`IRIS_FCM_SERVICE_ACCOUNT` JSON path) → mint a short-lived @@ -74,7 +77,7 @@ Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`). - Batch: FCM allows up to 500 tokens/message; we send per-device (1 user, few devices). -### 8.2.2 `NtfyBackend` (fallback, self-host friendly) +### 8.2.2 `NtfyBackend` (default, self-host friendly) - Reuses hermes's existing ntfy publish path (hermes ships an ntfy adapter). - Publish to `NTFY_TOPIC` on `NTFY_SERVER_URL` (default `https://ntfy.sh`) via diff --git a/docs/12-toolchain.md b/docs/12-toolchain.md index 583e8aa..110d871 100644 --- a/docs/12-toolchain.md +++ b/docs/12-toolchain.md @@ -77,15 +77,16 @@ hermes --version # sanity 4. The app's `FirebaseMessagingService` obtains the FCM token at runtime and registers it via `hello` / `fcm.register`. -> Skip Firebase → set `IRIS_PUSH_BACKEND=ntfy` and configure `NTFY_TOPIC` / -> `NTFY_SERVER_URL` (self-host ntfy or use ntfy.sh). See `08-push.md`. +> Skip Firebase → the default is already ntfy: leave `IRIS_PUSH_BACKEND` unset +> (or set it to `ntfy`) and configure `NTFY_TOPIC` / `NTFY_SERVER_URL` +> (self-host ntfy or use ntfy.sh). See `08-push.md`. ## 12.6 Environment variables (summary) **Secrets (`~/.hermes/.env`):** ``` IRIS_TOKEN=<64-hex> -IRIS_PUSH_BACKEND=fcm # or ntfy +IRIS_PUSH_BACKEND=ntfy # default; fcm = opt-in (metadata via Google) IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json # IRIS_FCM_SERVER_KEY= # fallback if no service account # NTFY_TOPIC=iris-push # when ntfy diff --git a/docs/16-open-questions.md b/docs/16-open-questions.md index 70bff70..084fcb3 100644 --- a/docs/16-open-questions.md +++ b/docs/16-open-questions.md @@ -5,7 +5,7 @@ | # | Decision | Choice | Rationale | | --- | --- | --- | --- | | 1 | Desktop app tech | **Compose Multiplatform** | Desktop = "the Android app, tweaked"; share protocol/state/UI. | -| 2 | Push backend | **Both — FCM primary, ntfy fallback** | FCM is standard/reliable; ntfy for self-hosters with no Firebase. `IRIS_PUSH_BACKEND`. | +| 2 | Push backend | **Both — ntfy default, FCM optional** (issue #10: FCM metadata — title, device token — is routed via Google's servers, contradicting the privacy claim; ntfy keeps it on your own infrastructure) | `IRIS_PUSH_BACKEND`. | | 3 | Media transport | **Over the WebSocket** | One transport, zero new Python deps; chunked binary frames. | | 4 | Phone default layout | **User-toggleable, single-pane default** | App-like on phones; auto two-pane on large screens; desktop defaults two-pane. | diff --git a/docs/README.md b/docs/README.md index 3799365..76b61e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,7 +30,7 @@ top-to-bottom once, then use the numbered docs as a lookup while implementing. | 5 | [`05-streaming.md`](05-streaming.md) | Streaming / reasoning / tools / intermediate. | | 6 | [`06-channels-cron-search.md`](06-channels-cron-search.md) | Channels, threads, cron delivery, search. | | 7 | [`07-media.md`](07-media.md) | Media upload/download + playback. | -| 8 | [`08-push.md`](08-push.md) | Push (FCM + ntfy), outbox, sync. | +| 8 | [`08-push.md`](08-push.md) | Push (ntfy default + FCM optional), outbox, sync. | | 9 | [`09-pairing-security.md`](09-pairing-security.md) | Pairing, auth, security model. | | 10 | [`10-android-app.md`](10-android-app.md) | When building the Android app. | | 11 | [`11-desktop-app.md`](11-desktop-app.md) | When building the Desktop app. | @@ -47,6 +47,7 @@ Machine-readable / diagrams: - [`protocol/frames.schema.json`](protocol/frames.schema.json) — wire-frame schema. - [`diagrams/architecture.mmd`](diagrams/architecture.mmd) — mermaid architecture. +- [`playstore-listing.md`](playstore-listing.md) — Play Store listing text (incl. the FCM/ntfy privacy note). --- diff --git a/docs/playstore-listing.md b/docs/playstore-listing.md new file mode 100644 index 0000000..ef87386 --- /dev/null +++ b/docs/playstore-listing.md @@ -0,0 +1,40 @@ +# Play Store Listing + +Copy-paste text for the Play Console (and the F-Droid / sideload pages). +Keep this file in sync with the README whenever the privacy story changes. + +## Short description (≤ 80 chars) + +Private chat for your personal AI agent — Android & desktop. + +## Full description + +Iris is a native chat app for your personal AI agent (hermes-agent): +streaming replies, visible reasoning, structured tool activity, channels, +threads, media, search — Telegram-quality, on your own infrastructure. + +**Absolute Privacy!** — everything stays on your own infrastructure: + +- Your gateway, your machine, your data. No cloud middleman for chat. +- **Push notifications:** ntfy by default — push metadata stays on your own + (self-hosted) ntfy server. +- **FCM is opt-in** (`IRIS_PUSH_BACKEND=fcm`): standard and reliable, but FCM + push metadata (notification title, device token) is routed through + **Google's servers**. If you want truly private communication, use ntfy + (self-hosted) instead — it is the default. + +No file limit. No character limit. Full markdown + HTML artifact preview. +All settings live in the app. + +Runs on Android and desktop (Linux, macOS, Windows) from one shared codebase. + +## Privacy note (for the "Data safety" section / FAQ) + +Iris talks directly to your own hermes gateway over a private, token-authenticated +connection. By default, push notifications use ntfy, which you can self-host so +that push metadata never leaves your infrastructure. If you explicitly enable +FCM, push metadata (notification title, device token) is sent via Google's FCM +servers; chat content itself is not sent to Google — FCM only carries a short +preview, and full content is fetched from your gateway over the authenticated +connection. For truly private communication, use the default ntfy backend +(self-hosted). diff --git a/docs/setup.md b/docs/setup.md index 0c54779..842029e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -39,7 +39,8 @@ What it does: - Generates `IRIS_TOKEN` (64 hex chars) if none exists and stores it in `~/.hermes/.env` (it prints the token once, at generation). - Prompts for the WS bind host (default `127.0.0.1`), port (default `8790`), - and push backend (`fcm` or `ntfy`, default `fcm`). + and push backend (`ntfy` or `fcm`, default `ntfy`); warns when `fcm` is + chosen (push metadata via Google's servers). - Prints the pairing payload (a QR-encodable `iris://pair?host=…&port=…&token=…` string), a scannable QR of that payload, and the server URL (`ws://:8790/ws`). @@ -100,26 +101,10 @@ Push wakes a backgrounded/offline device; on reconnect the app syncs the outbox, so nothing is lost. Push fires when the device is offline, plus for high-priority events (approvals, clarifies, cron) even when a device is live. -### FCM (default; needs a Firebase project) - -1. Create a Firebase project (console.firebase.google.com) and add an Android - app with the app's applicationId; download `google-services.json` into - `app/androidApp/`. -2. Create a service account (Project settings → Service accounts → Generate new - private key) and store the JSON path in `~/.hermes/.env`: - `IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json`. -3. Keep `IRIS_PUSH_BACKEND=fcm` (the default). - -Without a Firebase project the FCM path is **inert** (the app's FCM service -does nothing) — use ntfy below, or add Firebase later. - -**What you see:** system notifications for new messages when the app is -backgrounded; tapping one deep-links to the chat. - -### ntfy (zero-config fallback) +### ntfy (default; zero-config) ``` -IRIS_PUSH_BACKEND=ntfy +IRIS_PUSH_BACKEND=ntfy # the default — can be left unset ``` - The device **generates its own topic** automatically (no `NTFY_TOPIC` needed); @@ -128,10 +113,29 @@ IRIS_PUSH_BACKEND=ntfy recommended** — the public ntfy.sh SSE endpoint is flaky (it has served its web UI instead of the stream), while a self-hosted instance gives reliable SSE. For a real trust boundary use a private topic + `NTFY_AUTH_TOKEN`. +- **Privacy:** ntfy keeps push metadata (title, topic) on your own + infrastructure — this is the backend for truly private communication. **What you see:** a low-priority foreground "ntfy listener" notification while the app is off; incoming pushes trigger a silent sync. +### FCM (opt-in; needs a Firebase project) + +> **Privacy note:** FCM push metadata (notification title, device token) is +> routed through **Google's servers**. If you want truly private +> communication, use ntfy (self-hosted) instead — it is the default. + +1. Create a Firebase project (console.firebase.google.com) and add an Android + app with the app's applicationId; download `google-services.json` into + `app/androidApp/`. +2. Create a service account (Project settings → Service accounts → Generate new + private key) and store the JSON path in `~/.hermes/.env`: + `IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json`. +3. Set `IRIS_PUSH_BACKEND=fcm`. + +Without a Firebase project the FCM path is **inert** (the app's FCM service +does nothing) — use ntfy (the default), or add Firebase later. + ## 5. Remote access - **Tailscale / WireGuard (recommended):** the gateway gets a stable tailnet IP; diff --git a/gateway-plugin/adapter.py b/gateway-plugin/adapter.py index 4071d5c..721e6d7 100644 --- a/gateway-plugin/adapter.py +++ b/gateway-plugin/adapter.py @@ -33,8 +33,8 @@ register the (delivery-validated) file in the media registry and emit ``validate_media_delivery_path`` at pull time. Milestone M5: push + offline. Frames with no live subscriber are parked in -the outbox (M3) AND wake the device via the push backend (``push.py``: FCM -HTTP v1 primary, ntfy fallback, selected by ``IRIS_PUSH_BACKEND``). +the outbox (M3) AND wake the device via the push backend (``push.py``: ntfy +default, FCM HTTP v1 as an option, selected by ``IRIS_PUSH_BACKEND``). ``notification`` frames render in-app banners and mirror to push (channel events, cron deliveries, approvals, clarifies); high-priority kinds push even when a device is live. ``fcm.register`` rotates push tokens (registry + live @@ -526,7 +526,7 @@ DEFAULT_PORT = 8790 DEFAULT_HTTP_PORT = 8791 # docs/19: HTTP fallback leg DEFAULT_HOME_CHANNEL = "default" DEFAULT_HOME_CHANNEL_NAME = "Default" -DEFAULT_PUSH_BACKEND = "fcm" +DEFAULT_PUSH_BACKEND = "ntfy" DEFAULT_OUTBOX_RETENTION_HOURS = 72 DEFAULT_MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 MB @@ -1201,10 +1201,17 @@ def interactive_setup() -> None: ) save_env_value("IRIS_HTTP_PORT", str(_parse_port(port))) backend = prompt( - "Push backend (fcm/ntfy)", + "Push backend (ntfy/fcm)", default=get_env_value("IRIS_PUSH_BACKEND") or DEFAULT_PUSH_BACKEND, ) - save_env_value("IRIS_PUSH_BACKEND", (backend or DEFAULT_PUSH_BACKEND).strip().lower()) + backend = (backend or DEFAULT_PUSH_BACKEND).strip().lower() + save_env_value("IRIS_PUSH_BACKEND", backend) + if backend == "fcm": + print_warning( + "FCM push metadata (notification title, device token) is routed " + "through Google's servers. For truly private communication use " + "ntfy (self-hosted) instead." + ) # Pairing payload for the app's Connect screen (manual entry + QR scan). # Advertise a routable host: a bind wildcard (0.0.0.0/127.0.0.1) is @@ -1346,7 +1353,7 @@ class IrisAdapter(BasePlatformAdapter): # /fast): picker_id -> pending state. In-memory only — a gateway # restart expires them (a stale picker.select is a no-op). self._pending_pickers: dict[str, dict] = {} - # M5: push backend (FCM primary, ntfy fallback) + the throttle for + # M5: push backend (ntfy default, FCM optional) + the throttle for # the outbox-prune banner. self._push: PushBackend = build_push_backend( self.push_backend, diff --git a/gateway-plugin/plugin.yaml b/gateway-plugin/plugin.yaml index 5a9944d..fb2d467 100644 --- a/gateway-plugin/plugin.yaml +++ b/gateway-plugin/plugin.yaml @@ -38,7 +38,7 @@ optional_env: prompt: "Allow all devices? (true/false)" password: false - name: IRIS_PUSH_BACKEND - description: "Push backend: fcm (default) or ntfy" + description: "Push backend: ntfy (default, keeps metadata off Google) or fcm" prompt: "Push backend" password: false - name: IRIS_FCM_SERVICE_ACCOUNT diff --git a/gateway-plugin/push.py b/gateway-plugin/push.py index ffb92de..970f04a 100644 --- a/gateway-plugin/push.py +++ b/gateway-plugin/push.py @@ -1,4 +1,4 @@ -"""Push backends: FCM (primary) + ntfy (fallback). +"""Push backends: ntfy (default) + FCM (optional). ``PushBackend`` interface with two implementations: - ``FcmBackend``: FCM HTTP v1 via ``httpx`` + a Firebase service account @@ -8,7 +8,7 @@ (default ``https://ntfy.sh``) via ``httpx``; the app's listener subscribes to the topic. -Selected by ``IRIS_PUSH_BACKEND`` (``fcm`` default, ``ntfy`` fallback). +Selected by ``IRIS_PUSH_BACKEND`` (``ntfy`` default, ``fcm`` optional). Fired when a frame has no live subscriber; the data payload drives a silent sync on the device (docs/08-push.md). @@ -33,7 +33,9 @@ import httpx logger = logging.getLogger(__name__) FCM_SCOPE = "https://www.googleapis.com/auth/firebase.messaging" -FCM_TOKEN_URL = "https://oauth2.googleapis.com/token" +# Not a secret: the well-known Google OAuth2 token endpoint. +# pi-lens-ignore: S105 +FCM_TOKEN_URL = "https://oauth2.googleapis.com/token" # noqa: S105 FCM_V1_SEND_URL = "https://fcm.googleapis.com/v1/projects/{project_id}/messages:send" FCM_LEGACY_SEND_URL = "https://fcm.googleapis.com/fcm/send" # Refresh the cached access token this long before its expiry. @@ -54,7 +56,7 @@ class PushBackend: name: str = "push" # DeviceRegistry column that carries this backend's target token. # Not a secret: a DB column name (string literal), not a credential. - # pi-lens-ignore: python-hardcoded-secrets + # pi-lens-ignore: S105, python-hardcoded-secrets token_field: str = "" def configured(self) -> bool: @@ -87,8 +89,8 @@ class FcmBackend(PushBackend): name = "fcm" # Not a secret: a DB column name (string literal), not a credential. - # pi-lens-ignore: python-hardcoded-secrets - token_field = "fcm_token" + # pi-lens-ignore: S105 + token_field = "fcm_token" # noqa: S105 def __init__( self, @@ -257,8 +259,8 @@ class NtfyBackend(PushBackend): name = "ntfy" # Not a secret: a DB column name (string literal), not a credential. - # pi-lens-ignore: python-hardcoded-secrets - token_field = "ntfy_topic" + # pi-lens-ignore: S105 + token_field = "ntfy_topic" # noqa: S105 def __init__( self, @@ -332,9 +334,14 @@ def build_push_backend( ntfy_server_url: str | None = None, ntfy_auth_token: str | None = None, ) -> PushBackend: - """Select the backend by name (``IRIS_PUSH_BACKEND``; fcm default).""" - if (name or "").strip().lower() == "ntfy": - return NtfyBackend( - topic=ntfy_topic, server_url=ntfy_server_url, auth_token=ntfy_auth_token - ) - return FcmBackend(service_account=fcm_service_account, server_key=fcm_server_key) + """Select the backend by name (``IRIS_PUSH_BACKEND``; ntfy default). + + ntfy is the default: it keeps push metadata on your own infrastructure. + FCM is opt-in (``IRIS_PUSH_BACKEND=fcm``) — its metadata (title, device + token) is routed through Google's servers. + """ + if (name or "").strip().lower() == "fcm": + return FcmBackend(service_account=fcm_service_account, server_key=fcm_server_key) + return NtfyBackend( + topic=ntfy_topic, server_url=ntfy_server_url, auth_token=ntfy_auth_token + ) diff --git a/gateway-plugin/tests/test_android.py b/gateway-plugin/tests/test_android.py index 3006b50..f2b38f0 100644 --- a/gateway-plugin/tests/test_android.py +++ b/gateway-plugin/tests/test_android.py @@ -1404,7 +1404,7 @@ def test_push_backend_selection(plugin): push = plugin.push assert isinstance(push.build_push_backend("fcm"), push.FcmBackend) assert isinstance(push.build_push_backend("ntfy"), push.NtfyBackend) - assert isinstance(push.build_push_backend(None), push.FcmBackend) # default + assert isinstance(push.build_push_backend(None), push.NtfyBackend) # default assert isinstance(push.build_push_backend(" NTFY "), push.NtfyBackend) assert push.build_push_backend("ntfy", ntfy_topic="my-topic").configured() is True