# App Notifications

> Canonical path: `specs/app/app-notification.md`
>
> Terminology (App service actors, App Runtime API, and transports) is
> authoritative in [`specs/service/service-actor-platform.md`](../service/service-actor-platform.md).

App Notifications are App-authored message projections used by runtime-capable
Apps. Built-in users include Approval and Workflow; authorized external
Apps use the same contract. App Notifications provide durable idempotent
creation, update, resolution, withdrawal, expiry, and Human card actions.

## Authority

The service must have an approved, active App installation whose pinned
`capabilities.runtime` declaration and current grants authorize:

- `messages.write`;
- `app.notifications.write`;
- `app.notification_actions.read` when callback card actions are present;
- the requested notification target and content kind.

Notification ingress is restricted to either a server-registered internal
service identity or an authenticated external App runtime identity. The
external path additionally rechecks the active installation, pinned runtime
capability, current grants, workspace, and target binding. An ordinary member
session is not a notification credential.

## Internal API

The server-owned integration uses:

```text
POST  /internal/workspaces/:workspaceId/app-notifications
GET   /internal/workspaces/:workspaceId/app-notifications/:notificationId
PATCH /internal/workspaces/:workspaceId/app-notifications/:notificationId
POST  /internal/workspaces/:workspaceId/app-notifications/:notificationId:resolve
POST  /internal/workspaces/:workspaceId/app-notifications/:notificationId:withdraw
```

The internal credential maps to exactly one registered identity, installation slug, target allowlist, content allowlist, and optional action handler key. Workspace routing is explicit and checked before the request body is processed.

## External App runtime API

An approved callback or runner App whose pinned manifest grants notification
authority uses the authenticated runtime root:

```text
POST  /api/v1/workspaces/:workspaceId/app-runtime/notifications
GET   /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId
PATCH /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId
POST  /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId:resolve
POST  /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId:withdraw
```

Runner credentials authenticate App-level operations. A delivery token can
authorize only mutations tied to its addressed delivery.

## Notification model

Creation requires a client notification ID, target, and content. The pair of installation and client notification ID is idempotent: the same normalized payload replays the original result, while a different payload conflicts.

Supported targets:

- `human_dm` for a recipient established by an authoritative recipient-basis resolver;
- `scope`/bound scope for an installation already bound to that scope.

Supported content:

- `message`;
- `interactive_card`.

Every newly created notification remains an ordinary scope message and uses the
app Store's unified `autonomous_store_recoverable` committed-message lifecycle
defined by [`scope-model.md`](../message/scope-model.md). In the create
transaction, the Store persists the complete message delivery together with a
recoverable stream projection. Only after commit, the Store submits the fresh
handoff to the workspace's shared fanout, which applies current Human unread,
the `message.created` member stream event, the Inbox conversation index, and
durable outbox completion. A failed transaction or idempotent notification
create replay does not repeat this first fanout.

The canonical durable resource is `service_notifications`. Mutation receipts use
`service_notification_mutations`; interactive render/action state uses
`service_notification_cards` and `service_notification_actions`. Interactive
messages have `scope_messages.message_type='app_interactive_card'`, and
service-event envelopes identify the resource as `app_notification`.

Unread and durable replay follow the ordinary Channel/DM contract: the initial
post-commit attempt updates a warm runtime, while replay restores the stable
message stream and optional Inbox conversation projection without claiming
exact crash-window unread recovery. A `scope` target therefore follows its
Channel roster, while a `human_dm` target follows its Human peer and is
presented in the Inbox as a service DM.

Historical repair may reconstruct a missing Approval DM conversation index
row from the latest durable notification and message. Repair is idempotent and
must not synthesize unread state or member stream events; only new committed
messages produce those live effects.

An interactive card has a canonical schema version, title, and actions. Actions are either `open_url` or `callback`. Callback actions require stable action keys, current card revision, client action ID, and schema-validated input.

Approval is the canonical personal approval inbox. Team role proposals use
the same `human_dm` assignment-recipient contract as other approvals while
retaining their dedicated proposal/previous/diff card presentation. Team
channels do not host actionable approval cards.

## Human action flow

The authenticated Human reads or invokes a card through the workspace message surface. AgentTeam ignores actor identity supplied in JSON and derives the actor from the session and current message visibility.

For a callback action:

1. reserve an idempotent action invocation;
2. reserve a `service_event_delivery` routed to the registered `internal_handler`;
3. claim a durable delivery attempt;
4. invoke the built-in handler;
5. record a typed effect such as message update, notification resolution, or withdrawal;
6. complete the action and retire its one-shot subscription.

Retries and lease rollover reuse the same durable records. Uninstall, expiry, and explicit cancellation terminalize active action state without deleting notification history.

## Security boundaries

- The installation's pinned manifest is authoritative.
- Runtime handler keys are server configuration, never manifest input.
- Current installation, grant, target, recipient, message-visibility, and card-revision facts are rechecked at use time.
- Reserved AgentTeam metadata cannot be injected by notification content.
- Request bodies are bounded and decoded as exactly one JSON document.
- GitHub/Gitea provider webhooks and Agent-App MCP tokens are separate capabilities.
- External callback envelopes are HMAC signed, and their delivery bearer tokens
  remain scoped to one delivery attempt.

## Not part of this surface

This surface does not provide generic member-authenticated notification writes,
generic App webhooks, arbitrary callback actions, or a browser diagnostics and
cancellation control plane. External App credentials are issued only through
the approved App onboarding/runtime flow described in
[`app-runtime.md`](./app-runtime.md).
