The installer talks to a single HTTP endpoint for runner, plugin and data packages. Only the installer self-update still uses GitHub directly.
This document defines the request format, the headers, and the response shapes. For the broader architecture see architecture.md. For how a package ends up on disk see install-manifest.md.
- architecture.md – where the endpoint client sits in the installer.
- security.md – HTTPS, bearer tokens, install UUID privacy.
The base URL is taken from config.php under the project_api_url key. Every request goes to that base URL with package_type (and install_uuid) appended as query parameters.
Example: if project_api_url is https://packages.example.test/api, the installer requests:
GET https://packages.example.test/api?package_type=runner&install_uuid=0192f8e3-7c8e-7c2f-9d2a-...
POST https://packages.example.test/api (form body: package_type, install_uuid, optionally more)
In the local Docker setup the default endpoint is http://web-server/index.php and the dev token is oak-local-dev-token.
| Header | Value | Notes |
|---|---|---|
Accept |
application/json |
Only on the package list query. |
Authorization |
Bearer <project_api_token> |
Only if project_api_token is configured. |
X-Install-UUID |
The current install UUID | Always sent, alongside the same value as install_uuid query parameter. |
The install UUID is a UUID v7 generated by InstallUuidManager and stored in .env.local.
The installer sends a form-encoded body with the following fields:
| Field | Description |
|---|---|
type |
Legacy alias for package_type. Always set. |
package_type |
runner, plugin or data. |
install_uuid |
The current UUID v7 from .env.local. |
Implementation: ProjectPackageApiClient::request().
The endpoint can answer the package list query with either a plain JSON array or an object that has a packages key. The installer accepts both.
Plain array:
[
{
"package_type": "runner",
"package_id": "oak-runner",
"version": "1.2.3",
"channel": "stable",
"package_name": "oak/runner",
"archive_size": 1234567,
"archive_sha256": "f3d0e…",
"download_url": "https://packages.example.test/oak-runner-1.2.3.tar.gz",
"composer": {
"name": "oak/runner",
"extra": {
"oak-engine-runner": {
"version": "1.2.3",
"channel": "stable"
}
}
}
}
]Object with packages:
{
"packages": [ /* same shape as above */ ]
}The detail call (getPackage) is a filtered list call: the installer still hits the same endpoint with package_type and install_uuid, then filters by package_id and optional version in memory. There is no separate detail endpoint.
| Field | Type | Notes |
|---|---|---|
package_type |
string | runner, plugin, or data. The installer drops entries whose package_type does not match the client it is asking with. |
package_id |
string | The stable identifier of the package (used for filtering and for the install dir for plugin/data). |
version |
string | Semver version string, e.g. 1.4.2. |
channel |
string | stable, beta, … Used for display only. Defaults to unknown. |
package_name |
string | The human-readable name (often the composer name). |
archive_size |
int | Size in bytes. |
archive_sha256 |
string | SHA256 of the archive. |
download_url |
string | Absolute URL or path relative to project_api_url. |
composer |
object | Composer metadata. The installer reads extra.<package-type> for the version + channel. |
| Package type | Composer key in extra |
|---|---|
runner |
oak-engine-runner |
plugin |
oak-engine-plugin |
data |
oak-engine-data |
If extra.<key>.env.dir is set, the installer uses it as the install directory basename. Otherwise it falls back to the basename of composer.name. See resolvePackageInstallDirFromMetadata.
Once the installer has chosen a package from the list, it calls downloadPackage($package_id, $version). That issues a plain HTTP GET to download_url and streams the response body into a temp file. The temp file is then handed to ProjectPackageArchiveExtractor, which extracts it into the target directory.
The package list response is cached for 5 minutes in <target>/var/cache/packages/<hash>.json, where <hash> is derived from the base URL, the package type and the install UUID. The cache file is removed:
- Manually via the Refresh data button (
refresh_packagesPOST). - Automatically when its
mtimeis older than the TTL.
There is no shared cache between runner, plugin and data – each client has its own cache file.
- HTTP 4xx/5xx → the installer shows a generic error page. The endpoint should return JSON with an error message if possible.
- Curl errors (DNS, TLS, timeout) → the same generic error path with the curl error message.
- Empty or malformed response → throws
RuntimeExceptionand the user sees the error.
The installer never retries automatically. The user can hit Refresh data to retry.