# App System Spec

> Canonical path: `specs/app/app-system.md`
> Status: living-spec
> Scope: current App product facade over the Service Actor Platform, App UI surfaces, App-hosted Extension capability exposure, and provider webhook boundaries.
>
> Related specs:
> - [`specs/service/service-actor-platform.md`](../service/service-actor-platform.md)
> - [`specs/service/service-event-delivery.md`](../service/service-event-delivery.md)
> - [`specs/app/provider-watch-agent-mcp.md`](./provider-watch-agent-mcp.md)
> - [`specs/app/gitea.md`](./gitea.md)
> - [`specs/identity/identity.md`](../identity/identity.md)

## 1. Purpose

App is the current product surface for installed service actors. The durable
substrate is the Service Actor Platform. App-specific APIs and UI may keep App
wording, but installation, connection, grants, capabilities, target bindings,
webhook ingress, service members, and service-authored messages resolve through
service actor records.

The Apps product surface is the App Marketplace plus two sections: **App
Integrations / 应用集成** and **Service Accounts / 服务号**. App Integrations is
the UI category for user-manageable Apps. GitHub and Gitea are provider Apps
that receive external events through signed provider webhooks and expose
event-subscription and MCP tooling. External/custom Apps declare
`capabilities.runtime` and run through the App Runtime API. These are all Apps;
provider ingress and runtime support do not create product subtypes. Service
Accounts are a separate product family. The authoritative
terminology (entity, service kind, product category, capability, protocol,
transport) lives in
[`specs/service/service-actor-platform.md`](../service/service-actor-platform.md).

## 2. App facade over service actors

Current App surfaces include:

```text
/api/apps
App settings UI
App detail pages
GitHub/Gitea provider setup
GitHub/Gitea Extension actions
Provider webhook routes
```

These surfaces translate to service actor operations:

```text
apps / app registry             -> service_definitions(kind=app)
app installation                -> service_installations(kind=app)
app service member              -> service_members
app connection / OAuth          -> service_connections
app grants / agent authorization -> service_grants / service_principal_authorizations
app MCP and message capabilities -> service_capabilities
target app presence             -> service_target_bindings
```

The App facade must not become a second app-only source of truth once the service
actor substrate is available.

App catalog and manifest authoring is deliberately narrow. The product catalog
contains the built-in GitHub, Gitea, trusted search engine, and connectionless
arXiv definitions plus a narrow workspace-associated external App onboarding
path. AgentTeam does not expose
generic custom App definition/version editing, arbitrary manifests, generic
callbacks, generic App avatar authoring, or arbitrary runtime capabilities.
External App onboarding accepts the App identity, manifest, configuration, and
one supported callback or runner transport; AgentTeam validates and pins the
immutable manifest for approval and runtime authorization. It is not a
workspace-facing version or scope selector. A workspace-created App also has an attribution-owned catalog
visibility: private by default, or public to safe discovery by ordinary active
members of that same workspace. This workspace value is separate from global
App/service-definition visibility and never broadens management or secret
access. The installed App detail has narrow post-install surfaces for a
manager-authorized catalog-visibility mutation, display-name mutation, an
asset-backed avatar update, and active Human managers; none reopens generic
definition editing. Runtime
installation, uninstall, connection matching,
service authorization, webhook receipt, service event subscription, and
delivery use `service_*` / service-event records first and only project legacy
App rows at explicit compatibility boundaries.

## 3. App identity and authorship

Visible App messages are service-authored messages. The author is the installed
App service member. Message source explains why the App wrote the message:

```text
service_direct_post
provider_event -> service_event_delivery
```

App proactive notifications use `service_direct_post` and the ordinary
MessagePlan path. A notification that carries an interactive card stores its
visible fallback in the message and its versioned render/action state in the
message-owned App card sidecar. Human card actions enter the App runtime as
typed service-event deliveries. The complete contract lives in
[`app-notification.md`](./app-notification.md).

App identity must not be inferred from provider payloads, old watch ids, or
legacy trigger rows.

## 4. Capabilities

App capabilities are service capabilities:

```text
message.write
events.receive
subscriptions.manage
mcp.tools
app.notifications.write
app.notification_actions.read
```

GitHub/Gitea provider MCP tools are exposed only to authorized agents and are
rechecked server-side on every call.

First-party search engine Apps expose independently named Builtin Extensions.
Their product identity, connection, command, credential, and egress contracts
are defined by
[`specs/extension/search-engine-apps.md`](../extension/search-engine-apps.md).

The first-party arXiv App exposes a connectionless Builtin Extension with only
`search` and `get`; its result and catalog contract is defined by
[`specs/extension/arxiv-app.md`](../extension/arxiv-app.md).

## 5. Target bindings

A target binding records that an installed service may operate against a AgentTeam
target. It is not a normal human/agent membership and does not grant full history
read. It records delivery scope, permission-check scope, authority subject, and
a stable delivery contract.

## 6. Pull request subscriptions

GitHub/Gitea pull request subscriptions are App service event subscriptions, not
AgentTeam schedules and not automation runs. Management is described in
[`provider-watch-agent-mcp.md`](./provider-watch-agent-mcp.md). Delivery is
described in [`specs/service/service-event-delivery.md`](../service/service-event-delivery.md).

The user action can still be called watching a pull request, and MCP tools keep
stable action names, but storage, source projection, delivery, Web presentation,
and diagnostics use service event subscription, receipt, and delivery records.

## 7. Provider webhooks

Only the built-in GitHub and Gitea provider webhook routes are exposed as
provider ingress. They verify a configured provider connection, normalize provider events, and deliver
them through `service_event_subscriptions`. The separately authenticated App
callback/runner runtime handles only the events and scopes authorized by
`capabilities.runtime` and current grants, and only durable App
deliveries; it does not restore a generic custom App callback, unsubscribe
callback, or callback-subscription runtime.

## 8. Durable App product rules

Apps is a first-level workspace rail entry below Knowledge and no longer appears
in the workspace avatar menu. Its secondary sidebar owns service-account
navigation and one Apps Integration section that combines provider integrations
with manageable external Apps. The root secondary entry is labeled **App
Marketplace**. Entering Apps restores the last explicitly selected Apps
destination in that workspace, including an explicit return to App Marketplace;
when no restorable selection exists, it opens App Marketplace at `/apps`.
Existing App and installation REST resources and workspace routes retain their
stable paths; the unified label and navigation hierarchy do not imply a
wire-contract rename. The Marketplace collection uses an overlay scrollbar only
while the Human is scrolling and reserves no empty scrollbar gutter when its
content fits. Each Marketplace card is one whole-card navigation control; it
does not repeat that destination in a footer action or expose approval state in
the card. Card descriptions render at most two lines and truncate overflow.

App management details use the full width of the remaining workspace
content pane, subject to the pane's responsive padding. They do not introduce a
separate centered, fixed-width settings column.

The App facade keeps these product rules from the original App design while
moving storage and runtime ownership to service actors:

- Apps are messaging participants, not invisible background pollers. A service
  actor is visible through its installed service member only in the targets where
  an explicit target binding or App action exists.
- Apps Marketplace and App detail reads are cache-first. They do not poll while
  mounted and do not refetch on browser focus or network reconnect. App realtime
  events mark the relevant query families stale without replacing visible cached
  data; leaving and re-entering the Apps surface may then refresh stale data in
  the background. Successful user mutations may still patch or explicitly
  invalidate the exact affected projections.
- An ordinary workspace member may discover a public workspace-created App and
  invite it through either Apps or a channel's Add people surface only when the
  member currently has channel invitation authority. Making that App private
  removes it from every joined channel and channel-derived child scope, removes
  subscriptions, and disables channel-derived target bindings atomically;
  historical messages are retained. Public visibility never permits
  configuration, credential, manager, identity, deletion, Task collaborator,
  or Agent collaborator mutations.
- Subscription is explicit. Installing or connecting an App does not subscribe
  every channel, thread, DM, task, or agent context in the workspace.
- One App identity can have many provider connections. Connection credentials,
  webhook routing facts, and provider-side installation metadata are connection
  state, not visible author identity.
- Trusted built-in Apps are platform infrastructure, not user-managed Apps.
  Definitions using the internal-handler transport and installations carrying
  `metadata.builtin=true` are excluded from the App catalog, My Apps, and
  installed-App management surfaces. They remain visible only where they act as
  message authors, such as Approval conversations in Inbox.
- Provider webhooks and App MCP tools authorize into service capabilities and
  then use the same service actor or service event stores.
- Secret classes stay separate: provider connection credentials, provider
  webhook signing secrets, App credential master keys, and Agent-App capability
  tokens must not be interchangeable.

## 9. Lifecycle and grants

Workspace members may view the built-in App catalog, safe provider configuration
summary, active state, and active connections. For a built-in provider App's
install, uninstall, and governed connection changes, a normal member submits an
installation request and a workspace administrator approves or rejects it.
Active GitHub organization additions are the provider-specific exception
described below: an existing eligible provider installation is selected and
connected directly without either a GitHub owner reapproval or a second AgentTeam
approval. GitHub's signed installation flow is used only when the App is not
already installed on the desired organization. The requester and the first
installer have no extra authority over that provider App. A workspace
administrator may write directly, subject to the provider-specific request and
mutation fences below.
Reinstalling an App must not silently resurrect old connections, target
bindings, or Agent grants after an uninstall.

GitHub and Gitea share one Integration product contract for detail hierarchy,
safe reads, connection list states, administrator-direct mutations,
member-requested mutations, pending feedback, and approval settlement. Only the
provider operation itself differs: GitHub uses its signed App installation flow
and re-verifies the installation at approval time, while Gitea uses its
instance/token configuration and webhook lifecycle. GitHub's installed config
summary is read-only for ordinary members; Gitea configuration changes may be
submitted as approval requests. Provider secrets are never returned by these
safe reads or copied into browser-visible or submitted approval-request
projections. A setup draft or provider-mutation saga may retain only the
encrypted credential snapshot required for completion and recovery.

GitHub manifest registration is mediated by a AgentTeam HTTPS start page rather
than a browser/Electron-local manifest submission. The authenticated caller
requests a short-lived start URL with a whitelisted return target (`web` or
`desktop`). The signed start token is used only by AgentTeam's own start page; that
page validates current setup authority, sets the HttpOnly GitHub callback
cookie in the system browser, and posts the manifest to GitHub with only the
cookie nonce as GitHub `state`. The callback consumes the cookie, revalidates
current setup authority, exchanges the GitHub code, and persists either the
active config or the owner-bound setup draft server-side. Desktop opens only
the AgentTeam HTTPS start URL in the system browser; completion may notify Desktop
through `agentteam://github-setup-complete`, but that deep link is an untrusted
refresh/navigation hint. Desktop must confirm the result by re-reading existing
GitHub config or setup-proposal state, and provider codes, private keys,
webhook secrets, or client secrets must never cross the Desktop renderer
boundary.

For a fresh administrator setup that reuses an existing GitHub App, credential
verification first persists an encrypted, owner-bound, short-lived
`setup_ready` draft. This draft is a contender, not an App-id or workspace
reservation. AgentTeam then automatically performs read-only installation discovery
from that stored snapshot. This preview must not install the AgentTeam App, save an
active GitHub config, create or restore a connection, unblock a subscription,
or update the GitHub App webhook. The browser receives only the opaque draft
id, safe App identity, expiry, and the minimal installation id/account
login/target type of eligible organization installations. It must not
automatically select the first result. An installation is eligible for this
no-GitHub-approval reuse path only when its granted permissions and subscribed
events satisfy AgentTeam's current GitHub App manifest.

The administrator explicitly selects between one and fifty organizations and
submits **Install and connect**. The initial Begin transaction is the
authorization linearization point: it atomically binds that draft and fixed,
canonical target set to a short pre-mutation lease. Duplicate, empty,
non-canonical, or over-limit selections are rejected rather than silently
deduplicated. AgentTeam exactly re-reads every selected provider installation and
rejects an invalid canonical id, App id, an empty or stored-config-mismatched
App slug, missing account identity, non-organization target, suspended
installation, or missing required permission/event. The complete set must pass
before any provider mutation; one invalid target fails the whole operation.
Before the first outbound webhook PATCH, a database transaction must atomically
acquire the globally unique GitHub App-id/workspace claim for the exact
completion token and advance the completion saga to a durable `patching` state.
That Mark transaction—not draft creation, request submission, or provider
reads—is the fresh-install ownership linearization point.
`patching` has no TTL and must not be cancelled or garbage-collected merely
because the request ended or the initiating administrator's role changed.
After an authorized Begin, completion is a server-owned recovery operation;
later role changes prevent new privileged operations but do not strand this
already-authorized provider mutation. Membership removal still revokes that
principal's access; the durable saga remains available for trusted operational
repair rather than being deleted or reassigned to the removed caller.

A GitHub App id may be bound to only one AgentTeam workspace at a time. Every route
that PATCHes an existing App's global webhook—including fresh completion,
requester approval execution, installed-config repair, and active-config
update—must acquire or validate the same database claim and durable saga before
provider mutation. Read-only requester verification and discovery never PATCH
the provider. A conflicting workspace fails before the external PATCH. Drafts,
`setup_ready` rows, `setup_verifying` leases, and `pending` requests hold no
exclusive claim; multiple contenders may coexist in one workspace or reuse the
same App id across workspaces. The first exact token that completes Mark writes
a non-expiring `patching` claim. A GitHub registration callback cannot claim the
newly created App id until GitHub returns it. An administrator-direct callback
establishes the `active` claim atomically with its local activation; a requester
callback establishes only the encrypted private draft. A successful patching
saga promotes its exact claim to `active`; it remains owned until the GitHub
configuration or installation is deliberately removed.

Only after the durable `patching` transition may AgentTeam update the App-global
GitHub webhook with the stored secret. One local materialization transaction
must then install the AgentTeam App at the draft's pinned version, save the active
GitHub config, create or re-enable the complete selected first-connection set,
unblock only their matching subscriptions, and consume the draft while
promoting the claim to `active`. To preserve that postcondition in the presence
of a disabled canonical installation, the canonical reinstall cleanup must
remove or disable its residual connections, webhook endpoints, and
subscriptions before the selected connections are materialized;
the transaction verifies that the enabled connection-reference set exactly
equals the pinned selection.
Failure rolls back those local materialization changes but retains the durable
saga and claim for deterministic repair. Because the provider webhook update
cannot participate in the database transaction, every retry after a possibly
successful provider response must reuse the same encrypted secret and fixed
selection rather than rotate or switch them.

The legacy GitHub config-write endpoint is not a fresh-install shortcut: it
must reject a workspace with neither an installed GitHub App nor an active
config and direct that workspace through the draft, discovery, and explicit
selection flow. It may still repair an installed App whose config is missing,
or update an active config, provided the mode and prior config snapshot are
pinned transactionally and the global App-id claim and durable webhook-mutation
saga rules above are enforced.

An already-existing, eligible GitHub installation does not require another
GitHub organization-owner approval merely to connect it to AgentTeam. Installing the
App in a new organization, expanding its repository selection, or accepting new
permissions still uses GitHub's own installation or update flow.

For an ordinary member's **first** GitHub activation, both "create a new App"
and "reuse an existing App" converge on the same owner-bound setup draft and
single workspace approval:

1. Manifest registration or credential recovery stores only a private,
   encrypted `setup_ready` proposal. It does not
   install the AgentTeam App, save an active provider config, create a connection,
   or PATCH the provider webhook.
2. The proposal owner can discover eligible organization installations through
   a proposal-scoped read. A peer member, peer administrator, other workspace,
   expired owner, or changed role is rejected before credential decryption or
   provider I/O. The response is the same minimal installation id/account
   login/target-type projection used by the administrator draft.
3. A proposal with no eligible candidate cannot be submitted. For a newly
   registered App, an owner-bound proposal endpoint may create a GitHub
   organization-install URL using signed, expiring state bound to the user,
   workspace, proposal, and nonce. A callback with the original HttpOnly cookie
   only verifies the returned installation against that proposal and redirects
   to discovery; it neither activates AgentTeam nor creates a second approval
   request. An asynchronous organization-owner approval without that requester
   cookie must not guess a workspace or mutate local state. The requester later
   refreshes the same scoped discovery.
4. The member manually selects between one and fifty eligible installations and
   promotes the proposal to one immutable `pending` install request. That
   request carries a canonical, duplicate-free, numerically ordered provider
   installation-id set as its first-connection targets; a lost-response retry
   may recover the same request but may not add, remove, or switch targets.
5. One administrator approval exactly re-verifies the complete selected set,
   atomically wins or loses the App/workspace activation race at the durable
   provider-mutation Mark, PATCHes only after winning with the
   retained secret, and atomically installs the AgentTeam App, saves the config,
   promotes the App claim, and materializes exactly that enabled connection
   set. One failed or drifted target produces no partial local activation and no
   provider PATCH. The first connections are therefore part of the App install
   decision, not separate post-install connection requests.

The ordinary-member proposal and administrator-direct draft remain separate
owner-bound capabilities; neither may consume the other's draft. Conversely,
an ordinary member's unsubmitted private `setup_ready` draft, an administrator
draft, a short `setup_verifying` lease, and a submitted `pending` request are
not workspace-wide administration locks. Each caller may replace only their own
unsubmitted draft; peer drafts and ordinary-member pending audit rows remain
independent contenders. A pre-Mark administrator-direct callback may win by
atomically creating the `active` claim together with the installation and
config, after which retained pending requests deterministically become
cannot-apply on approval. Likewise, the first member approval or administrator
completion that wins Mark prevents every other contender from issuing a
provider PATCH. `setup_patching` is the sole durable pre-active fence: it must
not be deleted, expired, reassigned, rejected, or preempted by another callback.
Only an exact same-token retry with the same App id, secret, and immutable
target identities may resume it. Setup claims created by older AgentTeam versions
are non-authoritative reservations and are removed during migration; `patching`
and `active` claims are never removed by that cleanup.

For an active GitHub App, the detail page shows the authoritative connected
organizations and one **Add connection** action. It does not show a persistent
second provider-installation inventory, candidate-connection section, or
manual refresh control. Opening **Add connection** performs fresh read-only
discovery in a transient dialog and lists eligible provider installations that
are not currently connected to this AgentTeam workspace. No result is selected
automatically, including when only one candidate exists. Any active human
workspace member may explicitly choose exactly one existing installation and
connect it directly without a AgentTeam approval request or another GitHub
organization-owner approval. Closing the dialog discards its candidate and
selection state.

The transient dialog also provides a separate **Install on another GitHub
organization** action. Only that explicit action starts GitHub's signed
installation flow; an empty or failed discovery must never redirect there
automatically. The callback verifies the returned organization installation
and creates exactly one AgentTeam connection directly, without a AgentTeam approval
request. Additional organizations use independent operations rather than a
batch. The active-connect mutation accepts exactly one canonical installation
id, exactly verifies that provider installation before mutation, then rechecks
the current App, config, claim, installation, connection, and member snapshots
under the canonical GitHub mutation lock before one atomic local transaction.
A disabled historical AgentTeam connection is not a provider-installation ban: an
explicitly selected installation that still exists upstream may be materialized
as a new active connection generation while the disabled row remains audit. A
lost-response retry for the same already-active target returns the current
desired state, while a different or drifted identity conflicts.
Disconnect remains a separately governed operation: an ordinary member submits
one `disconnect` request for an enabled connection and an administrator approves
it, while an administrator may disconnect directly. Ordinary members cannot
directly disconnect a connection or mutate the global provider configuration.
After the GitHub App is uninstalled, no pending `install` request blocks fresh
member setup; every such request remains only a contender until Mark. Pending
`connect` or `disconnect` approvals from an older App generation must be
terminalized as superseded audit rather than being treated as an installation
fence or becoming actionable after reinstall.

Workspace-created external Apps use the role-specific admission and active App
manager model in [`app-runtime.md`](./app-runtime.md); their creator is the
first active manager. External App display-name changes use that same active
manager authority and have no separate member-request surface.

The App display-name write is workspace-scoped in authorization even though
App definitions are global records. It must update the App definition,
canonical `service_definitions` row, `service_members` identity, and
service `members` profile atomically. A definition associated with more than
one workspace cannot be renamed through this surface, because doing so would
cross workspace authority. Slug, description, avatar, manifest, version, capabilities, and transports
remain immutable from this surface. The pinned manifest and capability scope
ceiling is immutable too; an active App manager or workspace administrator may
narrow the active grant subset through the App detail permission surface
(see [app-runtime.md](./app-runtime.md)) but can never expand it beyond
that pinned ceiling.

Built-in App definitions and their immutable manifest snapshots have no
product review status. Approval is a separate workspace-scoped lifecycle on
`service_installation_requests`; its `pending`, `approved`, and `rejected`
states remain authoritative and must not be projected back onto a definition or
version. Pending and rejected requests do not change active installation,
configuration, or connection state and do not call the provider. Proposal
details are visible only in the administrator approval surface. The requester
receives a minimal Approval status receipt containing only the durable
request identity, App name, and operation; it must not copy proposal config,
provider targets, the request reason, runtime endpoints, credential metadata,
scopes, or approval actions into the requester Inbox. Credentials are always
projected as a fixed mask.

Agent access is opt-in per App installation. An agent sees provider App MCP tools
only when the service installation is enabled, the connection is healthy, the
agent is granted access, and the target action passes server-side authorization.
The server rechecks those conditions for every MCP call; UI or prompt injection
is advisory only.

## 10. Runtime safety

Inbound provider webhooks must resolve a configured service connection before
trusting payload content, verify the provider signature or equivalent secret, and
reserve an idempotency key before writing effects. Service-authored messages
participate in normal channel, thread,
reply, unread, reaction, and task visibility; App messages must not recursively
trigger other App deliveries unless a future spec explicitly defines that agentteam.

Delivery code must expose diagnostics for connection blocking, rate limiting,
content rendering failures, permission failures, and provider retry state.
Pull-request subscription rate limiting is a canonical control-plane behavior;
it may use retained compatibility storage during cutover, but runtime errors,
identifiers, retention jobs, and guardrails must use pull-request subscription
or service-event vocabulary. Operator diagnostics may include provider delivery
ids and compatibility aliases, but the primary record must be the service actor,
service event delivery, or direct service-post audit record.

## 11. Compatibility

Legacy App, trigger, and watch identifiers are compatibility inputs. They are
translated by the compatibility contract and must not be used as the primary App
model. Old desktop or daemon wire compatibility is likewise derived at explicit
legacy boundaries.
