Skip to content

M6-08 Manage apps, webhooks and incoming webhooks

No Date Ruling Reason
D-M6-08-0 2026-10-22 Built on feat/M6-08-apps-webhooks-admin from origin/main (M2-26, M2-27 and M6-04 merged); rulings and changelog are fragments. Names that differ from the brief: the screens live in packages/admin_ui/lib/src/apps/ (AppsList, AppPage, InstallAppDialog, UpdateAppDialog, AppLifecycleDialog, AppSubscriptionsSection, DeliveriesView, WebhookKeysCard) and lib/src/incoming_webhooks/ (IncomingWebhooksView), the data source is AppsRepository (ApiAppsRepository, part of AdminRepository), the incoming webhooks are the place AdminSection.webhooks (/admin/incoming-webhooks on the desktop, /apps/incoming-webhooks in the console), and the patrol suites are a7_apps_test.dart and a8_incoming_webhooks_test.dart in apps/console/integration_test/admin/. The desktop's file dialog is file_selector (BSD-3-Clause, flutter.dev) with the sandbox entitlement files.user-selected.read-only; the console uses a file input of the browser. Instructions of the launcher; merged names win (D-W-100).
D-M6-08-1 2026-10-22 Install flow: the owner picks a manifest file (JSON, or YAML converted to JSON on the device, 11 3.1); a JSON file is sent with its exact text, so the server hashes the bytes the owner chose (D-M2-24-17), and every manifest reaches the server as text, so an unknown member meets the closed schema instead of being dropped by a typed model. validateManifest decides validity; its rules are shown with pointer, code and detail, a file the device cannot read (no UTF-8, neither JSON nor YAML, no object) is named before any request. The consent shows scopes with their meaning from 05 2.6 (required ones fixed, optional ones and the events checked and changeable), the target hosts of events_url and interactions_url, commands, components, the requested grants with a target picker each (or "do not create"), data_egress, the token's name, last day and optional DPoP key, and the reason. Because previewGrant runs the apply step of createGrant and refuses an unknown subject, a grant of an app that is not installed cannot be previewed: "Install" runs installApp, then the server's preview text of every chosen grant is shown and "Create grants and token" runs createGrant per target, createToken and getWebhookKeys; the token appears once with the key ID of the active key. A failed step stops the flow; the progress lists what succeeded and the server's reason, "Retry" repeats the failed step and the rest, "Uninstall" runs uninstallApp; nothing is retried silently. 11 4.1 says so. Brief Design note 1; 11 4.1; D-M2-24-8.
D-M6-08-2 2026-10-22 Update and lifecycle: a new manifest is validated, then the diff shows added and removed scopes, events, target hosts, requested grants and commands, says that removed items take effect at once and that new hosts are confirmed by this update; upgradeAppManifest keeps the granted scopes and subscriptions the new manifest still names; when the new manifest adds scopes or events, the new consent (setAppScopes, app.scopes.update) follows with its own confirmation; an update never adds a grant (added requests are created in Access grants). Suspend, resume and uninstall show the column of the table of 11 4.3 for their action before the confirmation; uninstall asks a reason. Brief Design note 2; 11 4.2, 4.3.
D-M6-08-3 2026-10-22 Outgoing webhooks: the subscription form offers only the granted events of the installation (getApp) and enables "Create" only for an https address; the host check is the server's (webhook_host_not_allowed is shown). Deliveries show status, attempts, next attempt, the last status code and error code, event type and sequence, never a response body (not stored, 11 7); "Redeliver" calls redeliverWebhook. A disabled subscription shows when and why from the server's values, because WebhookSubscription has no cause member: the newest finished delivery (failed or dead; pending rows of later events can stand before it, because the list is ordered by event_seq descending) with last_error = event_expired is the gap, a failed one (410) the receiver's refusal, otherwise the count of failures in a row. The keys for receivers come from getWebhookKeys with a copy action. Brief Design note 3; 11 6, 7; D-M2-26-5, D-M2-26-6.
D-M6-08-4 2026-10-22 One dialog shows every secret returned once (app and agent tokens, the address of an incoming webhook with its token, the HMAC secret of a subscription): OnceOnlySecret keeps it in its own state only, drops it on "Close", is not dismissed by a tap outside, keeps it out of the semantics tree until the focus is on it and passes it to no logger, crash report, local database or browser storage; TokenSecretView of the agents uses it. The "incoming app" of 11 8.1 has no operation of its own (IncomingWebhookCreate requires app_id), so for owners (getMe.is_owner) the dialog composes installApp with a built-in manifest (incoming-<8 random characters>, only messages:write, no events, no hosts, min_api_version 0.2), createGrant with write on exactly the chosen channel and createIncomingWebhook with that app under one confirmation; a failure after the install offers to retry or to uninstall the app. Managers choose an existing app. The gap stays in the report for a later spec change. Brief Design notes 4 and 5; 08 12; 11 8.1.
D-M6-08-5 2026-10-22 The patrol suites a7 (install from a manifest file, the token once, a subscription to the receiver and a succeeded delivery) and a8 (incoming webhook with the composed app, a post with the shown address, the message read through the API, revoke, the next post 404) run in phase 2 of make console-e2e. The console's file input carries data-bothy-file-input="manifest"; a patrol tap is no user activation, so the suite asks the runner for the file through the custom platform action e2eSetManifestFile (name, MIME type, text), sets it on that input with a DataTransfer and fires the input's change event, which reaches the console; the runner intercepts the page's file choosers with a listener that does nothing, because without one headless Chromium answers the input's click() with an immediate cancel and the console removes the input. The runner answers without touching the page: inside the pending page.evaluate of a test a nested call of Playwright's sync API never returns (an earlier set_input_files there hung phase 2 until the job timeout). The receiver webhook-receiver is the pinned Caddy image of the TLS sidecar on port 8443 with a leaf of the per-run test CA, answering every POST with 200 and logging method, path and headers (no body), with no published port (D-M6-03-7); the runner fails phase 2 unless it logged a 2xx POST with a webhook-id header. Deliveries run in the worker role (jobs), so phase 2 starts worker too, and both api and worker get webhook.allowed_hosts = webhook-receiver:8443, egress.allowed_hosts = webhook-receiver:8443;allow_private and SSL_CERT_FILE with a bundle of the roots of the server image (copied out with docker create and docker cp) and the test CA, only in the overlay of phase 2; the release image and the bundle are unchanged. Brief Change items for infra/compose/test and tools/console-e2e; M6-03, M6-04 D-M6-04-4.