Skip to content

Commit 9d321ad

Browse files
authored
Merge pull request #911 from nextcloud/docs/appapi-agents-md
docs: Add an AGENTS.md guide for installing, operating and developing AppAPI
2 parents 35c0603 + f112fec commit 9d321ad

7 files changed

Lines changed: 1175 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 571 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
# CLAUDE.md
7+
8+
Guidance for AI agents working in this repo lives in [`AGENTS.md`](AGENTS.md), the tool-agnostic convention.
9+
This file exists only so Claude Code, which reads `CLAUDE.md`, picks that guidance up automatically through the
10+
import below. Keep the real content in `AGENTS.md`; do not duplicate it here.
11+
12+
@AGENTS.md

docs/appapi/aio.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
# ExApps on Nextcloud AIO
7+
8+
A short runbook (a spoke of [`../../AGENTS.md`](../../AGENTS.md)). Read this when the Nextcloud instance is
9+
**AIO (all-in-one)**. AIO manages the deploy daemon for you: most of the manual setup in the `AGENTS.md`
10+
Quickstart does not apply, and fighting the managed daemon is the main failure mode.
11+
12+
## What AIO automates
13+
14+
AppAPI detects AIO via the `THIS_IS_AIO` env var and auto-registers the deploy daemon during app
15+
install/upgrade (`lib/DeployActions/AIODockerActions.php`, `lib/Migration/DataInitializationStep.php`):
16+
17+
- With AIO's **HaRP container** enabled (`HARP_ENABLED` + `HP_SHARED_KEY` set by AIO), AppAPI registers
18+
**`harp_aio`** (`nextcloud-aio-harp:8780`, network `nextcloud-aio`, HaRP direct-connect mode) and makes it
19+
the default. Exists on NC33+.
20+
- With the legacy **Docker Socket Proxy** container, AppAPI registers **`docker_aio`**
21+
(`nextcloud-aio-docker-socket-proxy:2375`). Deprecated since NC34, removal targeted for NC35.
22+
- If both are enabled, `harp_aio` wins the default. Registration is idempotent and AppAPI never removes a
23+
daemon it registered: after a DSP-to-HaRP migration, `docker_aio` stays behind; clean it up with
24+
`occ app_api:daemon:unregister docker_aio` once no ExApps use it.
25+
- AIO's own web server container routes `/exapps/` to HaRP internally, so the `AGENTS.md` Quickstart Step 4
26+
proxy rule is only needed on a reverse proxy **in front of** AIO, if you run one.
27+
28+
## What the admin does
29+
30+
1. In the AIO interface, enable the **HaRP community container** (or add `harp` to the
31+
`AIO_COMMUNITY_CONTAINERS` env var of the mastercontainer), then restart the AIO containers so the env
32+
reaches the Nextcloud container.
33+
2. That is all for the daemon: check **Settings --> Administration --> AppAPI** shows the `AIO HaRP` daemon,
34+
and use **Check connection** / **Test deploy**.
35+
3. Install ExApps normally (UI store, or `occ app_api:app:register <appid> --wait-finish`; on AIO run occ as
36+
`docker exec -u www-data nextcloud-aio-nextcloud php occ ...`).
37+
38+
## What NOT to do on AIO
39+
40+
- Do **not** run your own HaRP container or hand-register another HaRP daemon; `harp_aio` is AIO-managed
41+
(a second daemon competes with it, and re-registering the same name is a no-op).
42+
- Do **not** edit `harp_aio`'s host or shared key by hand; AIO owns `HP_SHARED_KEY` and a mismatch breaks
43+
Nextcloud-to-HaRP auth.
44+
- Do not re-default the deprecated `docker_aio`; migrate to HaRP and unregister it.
45+
46+
## Troubleshooting
47+
48+
- **"AppAPI default deploy daemon is not set"** on AIO: the HaRP community container is not enabled or not
49+
running, so `HARP_ENABLED`/`HP_SHARED_KEY` never reached the Nextcloud container and auto-registration
50+
skipped itself. Enable the container, restart AIO, and re-check.
51+
- **HaRP enabled but no `harp_aio` daemon**: the Nextcloud container was not recreated after enabling HaRP,
52+
so the env vars are missing inside it. Restart/recreate the AIO containers; the (idempotent) registration
53+
re-runs on the next app_api install/upgrade repair step.
54+
- **ExApp installs fine but its page is blank / WebSocket fails, only behind your own proxy**: the reverse
55+
proxy in front of AIO does not forward `/exapps/` (with WebSocket upgrade). The internal checks pass anyway;
56+
add the rule from `AGENTS.md` Quickstart Step 4 pointing at AIO.
57+
58+
For everything else (lifecycle, troubleshooting, version notes) see [`../../AGENTS.md`](../../AGENTS.md).

docs/appapi/exapp-contract.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
# The ExApp manifest (`<external-app>`) reference
7+
8+
A detailed runbook (a spoke of [`../../AGENTS.md`](../../AGENTS.md)). Read this when writing an ExApp's
9+
`info.xml`, preparing a `--json-info` payload, or debugging why a route, env var, or mount did not behave as
10+
expected. The runtime side (env vars AppAPI injects, init/heartbeat, auth headers) is in the hub's
11+
"Runtime and the ExApp contract" section.
12+
13+
Parsing lives in `ExAppService::getAppInfo()` (`lib/Service/ExAppService.php`); route validation in
14+
`lib/Service/ExAppRouteHelper.php`. Invalid route definitions abort registration with a descriptive error.
15+
16+
## The element tree
17+
18+
An ExApp's `info.xml` is a normal Nextcloud app manifest plus an `<external-app>` section:
19+
20+
```xml
21+
<external-app>
22+
<docker-install>
23+
<registry>ghcr.io</registry> <!-- default: docker.io -->
24+
<image>nextcloud/my-exapp</image> <!-- default: the app id -->
25+
<image-tag>latest</image-tag> <!-- default: latest -->
26+
</docker-install>
27+
28+
<routes>
29+
<route>
30+
<url>^/api/.*</url> <!-- required; regex on the request path -->
31+
<verb>GET,POST</verb> <!-- required; comma list -->
32+
<access_level>USER</access_level> <!-- required; PUBLIC | USER | ADMIN (or 0|1|2) -->
33+
<bruteforce_protection>[401,429]</bruteforce_protection> <!-- optional; JSON int array -->
34+
<headers_to_exclude>["Cookie"]</headers_to_exclude> <!-- optional; JSON string array -->
35+
</route>
36+
</routes>
37+
38+
<environment-variables>
39+
<variable>
40+
<name>MY_SETTING</name> <!-- key; required -->
41+
<display-name>My setting</display-name>
42+
<description>Shown in the UI</description>
43+
<default>some-value</default> <!-- becomes the value unless overridden -->
44+
</variable>
45+
</environment-variables>
46+
47+
<k8s-service-roles> <!-- optional; Kubernetes multi-Deployment apps -->
48+
<role>
49+
<name>api</name> <!-- role id / Deployment suffix -->
50+
<display-name>API</display-name> <!-- optional; defaults to name -->
51+
<env>SERVICE_ROLE=api</env> <!-- extra env line for this role's container -->
52+
<expose>true</expose> <!-- true = gets a Kubernetes Service -->
53+
</role>
54+
</k8s-service-roles>
55+
</external-app>
56+
```
57+
58+
With `occ app_api:app:register --json-info`, the same keys are given as JSON (`docker-install`, `routes`,
59+
`k8s-service-roles` may sit at the JSON root). Note the naming split: elements are hyphenated
60+
(`docker-install`, `image-tag`, `display-name`), but route fields are underscored (`access_level`,
61+
`bruteforce_protection`, `headers_to_exclude`).
62+
63+
Things the manifest does **not** declare:
64+
65+
- **Port and secret**: AppAPI assigns a free `APP_PORT` and generates `APP_SECRET` at registration.
66+
- **API scopes**: removed from AppAPI; ExApps no longer declare scopes anywhere (`--force-scopes` is a
67+
deprecated no-op). Do not add a scopes element.
68+
- **Mounts**: there is no `<mounts>` element (see below).
69+
70+
## Routes: access levels and enforcement
71+
72+
Every HTTP surface the ExApp exposes must be covered by a `<route>`; unmatched requests are rejected.
73+
74+
| Field | Meaning |
75+
|---|---|
76+
| `url` | Case-insensitive regex matched against the request path (e.g. `^/api/.*`) |
77+
| `verb` | Comma-separated HTTP methods the route accepts |
78+
| `access_level` | `PUBLIC` (0) anyone, `USER` (1) any logged-in user, `ADMIN` (2) admins only |
79+
| `bruteforce_protection` | JSON array of response status codes that count as a bruteforce attempt (e.g. `[401,429]`) |
80+
| `headers_to_exclude` | JSON array of request header names stripped before forwarding |
81+
82+
Enforcement happens in two places, depending on the path a request takes:
83+
84+
- **HaRP path** (browser to `/exapps/...`): AppAPI hands HaRP the route table (url + access_level +
85+
bruteforce_protection) via the ExApp metadata endpoint, and HaRP resolves the caller's level per request
86+
through the user-info endpoint (no/disabled user = PUBLIC, admin = ADMIN, else USER), enforcing the
87+
comparison itself. `verb` and `headers_to_exclude` are not part of the HaRP checks.
88+
- **PHP proxy path** (`/index.php/apps/app_api/proxy/...`): `ExAppProxyController` matches `url` (regex) and
89+
`verb`, enforces the access level, strips `headers_to_exclude`, and applies bruteforce throttling on the
90+
listed status codes.
91+
92+
Authoring notes: an empty element (`<headers_to_exclude></headers_to_exclude>`) is fine and means "none", but
93+
nested sub-elements (`<bruteforce_protection><status>401</status></bruteforce_protection>`) are rejected; use
94+
the JSON-in-text form shown above.
95+
96+
## Environment variables: a declared allow-list
97+
98+
`<environment-variables>` is an **allow-list** with defaults, not free-form input:
99+
100+
- Each declared `<variable>` starts with `value = default`.
101+
- `occ app_api:app:register --env NAME=VALUE` overrides **only declared names**; undeclared `--env` values are
102+
**silently dropped**. If the manifest has no `<environment-variables>` block at all, every `--env` is
103+
ignored.
104+
- Variables whose final value is an empty string are not passed to the container at all.
105+
- The surviving set is stored and replayed on `app:update` (no need to repeat `--env`).
106+
107+
So "my `--env` did nothing" almost always means the variable is not declared in the manifest.
108+
109+
## Mounts: CLI-only, Docker-only
110+
111+
There is **no** mounts element in the manifest. `occ app_api:app:register --mount SRC:DST[:ro|rw]`
112+
(repeatable, default `rw`) is the only source of extra mounts, and every given mount is applied as a bind
113+
mount on Docker daemons. (The `--mount` help text mentions a manifest declaration; no such gate exists in the
114+
code today.) On **Kubernetes**, mounts are recorded with the deploy options but **not** mounted into pods;
115+
persistent data goes through the PVC/`APP_PERSISTENT_STORAGE` instead. Mounts are replayed on `app:update`.
116+
117+
## Kubernetes service roles
118+
119+
For multi-process ExApps on Kubernetes, each `<role>` becomes its **own Deployment** (same image, plus the
120+
role's `<env>` line). Only roles with `<expose>true</expose>` get a Kubernetes Service, and the **first**
121+
exposed role is the app's single entry point for HaRP routing; other roles are internal-only. An ExApp without
122+
roles gets one default Deployment. On Docker daemons the roles element is ignored.
123+
124+
## Minimal working example (JSON form)
125+
126+
The shape used by the project's own Kubernetes tests, as an `--json-info` payload:
127+
128+
```json
129+
{
130+
"id": "app-skeleton-python",
131+
"name": "App Skeleton Python",
132+
"version": "1.0.0",
133+
"docker-install": {
134+
"registry": "ghcr.io",
135+
"image": "nextcloud/app-skeleton-python",
136+
"image-tag": "latest"
137+
},
138+
"k8s-service-roles": [
139+
{"name": "api", "env": "SERVICE_ROLE=api", "expose": true},
140+
{"name": "worker", "env": "SERVICE_ROLE=worker", "expose": false}
141+
]
142+
}
143+
```
144+
145+
A complete XML reference manifest lives in the `nextcloud/test-deploy` repository (the app the admin UI's
146+
**Test deploy** button installs). For building the ExApp itself, see **nc_py_api**
147+
(https://github.com/cloud-py-api/nc_py_api).

0 commit comments

Comments
 (0)