Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,8 @@ components:
HealthResponseBody:
additionalProperties: false
properties:
api_schema_version:
type: string
db_path:
type: string
ok:
Expand Down
83 changes: 83 additions & 0 deletions docs/reference/http-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# HTTP API schema

The daemon exposes an HTTP API (used by the CLI, TUI, and remote clients). Its
shape is published as an OpenAPI 3.1 document so out-of-process clients can
generate typed clients instead of hand-copying wire structs.

## Getting the schema

The schema is committed at [`api/openapi.yaml`](https://github.com/kenn-io/kata/blob/main/api/openapi.yaml)
and regenerated by the `kata` binary:

```sh
kata openapi > openapi.yaml
```

`kata openapi` builds the document in-process from the daemon's route
definitions, so it needs neither a running daemon nor a database. The runtime
`/openapi.json` route stays disabled; the committed artifact and this command
are the supported way to obtain the schema.

## What the schema is

A **per-release snapshot** of the daemon's current HTTP API — a faithful
description of the routes and wire types as they exist at that commit. It is
useful for client generation and review.

It is **not** a promise of a forever-stable API. Treat it as the contract for
the daemon version you generated it against, not a guarantee that a future
daemon will accept the same calls.

## Detecting the API version

The schema carries a version in its `info.version` field
(`APISchemaVersion`). The same value is reported at runtime by
[`GET /api/v1/health`](#) as `api_schema_version`:

```json
{
"ok": true,
"schema_version": 7,
"api_schema_version": "0.1.0",
"version": "1.4.2",
"uptime": "5m0s",
"db_path": "/path/to/kata.db"
}
```

Three distinct version fields appear here; they answer different questions:

| Field | Meaning |
| --- | --- |
| `api_schema_version` | The HTTP API contract version — match this against the schema you generated your client from. |
| `schema_version` | The database/storage schema version (`meta.schema_version`), an internal storage concern. |
| `version` | The daemon build version. |

A client can read `api_schema_version` once at startup and decide whether it
recognizes the daemon's API before issuing further calls.

The field is **optional in the schema** even though current daemons always send
it. That is deliberate: a version-detection field has to survive version skew,
so a client generated from a schema that includes it can still parse the
response of an older daemon that predates it. Treat an **absent or empty**
`api_schema_version` as "a daemon older than this field," not a parse error.

## Compatibility expectations

These are the current intentions, not a contractual guarantee:

- **Additive changes are not signalled.** New endpoints, and new optional
response fields, can appear without an `api_schema_version` change. Clients
should ignore unknown fields rather than fail on them.
- **Breaking changes bump `api_schema_version`.** Removing or renaming a field,
changing a field's type, or removing an endpoint is a breaking change and is
signalled by a change to `api_schema_version`. A client that pins or checks
the value it was generated against can detect the mismatch instead of failing
at an arbitrary call site.
- **Regeneration stays honest.** A committed golden test fails if
`api/openapi.yaml` drifts from the routes, so the published schema cannot
silently fall out of date with the daemon it describes.

If you build against this schema and hit a gap, please open an issue — the
contract is meant to be useful to external clients, and feedback shapes how far
the compatibility guarantees are taken.
1 change: 1 addition & 0 deletions docs/zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ nav = [
{"CLI" = "reference/cli.md"},
{"Configuration" = "reference/configuration.md"},
{"Agent output format" = "reference/agent-output.md"},
{"HTTP API schema" = "reference/http-api.md"},
]},
{"Workflows" = [
{"Agent workflows" = "workflows/agents.md"},
Expand Down
23 changes: 17 additions & 6 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@ type PingResponse struct {
}

// HealthResponse mirrors /api/v1/health.
//
// SchemaVersion is the database/storage schema version (meta.schema_version);
// APISchemaVersion is the version stamped into the daemon's OpenAPI document,
// letting an external client detect the HTTP API contract it is talking to.
//
// APISchemaVersion is schema-optional (omitempty) on purpose: the field exists
// to detect version skew, so it must itself survive it. A client generated from
// a schema that carries this field still needs to parse the response of an older
// daemon that predates it — an absent value means "older than this field" rather
// than a parse failure. Current daemons always populate it.
type HealthResponse struct {
Body struct {
OK bool `json:"ok"`
DBPath string `json:"db_path"`
SchemaVersion int `json:"schema_version"`
Version string `json:"version"`
Uptime string `json:"uptime"`
StartedAt time.Time `json:"started_at"`
OK bool `json:"ok"`
DBPath string `json:"db_path"`
SchemaVersion int `json:"schema_version"`
APISchemaVersion string `json:"api_schema_version,omitempty"`
Version string `json:"version"`
Uptime string `json:"uptime"`
StartedAt time.Time `json:"started_at"`
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/daemon/handlers_health.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func registerHealthHandlers(humaAPI huma.API, cfg ServerConfig) {
out.Body.OK = true
out.Body.DBPath = cfg.DB.Path()
out.Body.SchemaVersion = schema
out.Body.APISchemaVersion = APISchemaVersion
out.Body.Version = version.Version
out.Body.StartedAt = cfg.StartedAt
out.Body.Uptime = time.Since(cfg.StartedAt).Round(time.Second).String()
Expand Down
12 changes: 8 additions & 4 deletions internal/daemon/handlers_health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ import (

"github.com/stretchr/testify/assert"

"go.kenn.io/kata/internal/daemon"
"go.kenn.io/kata/internal/db"
)

func TestHealth_ReportsSchemaAndUptime(t *testing.T) {
ts, _ := startDefaultTestServer(t)

var body struct {
OK bool `json:"ok"`
SchemaVersion int `json:"schema_version"`
Uptime string `json:"uptime"`
DBPath string `json:"db_path"`
OK bool `json:"ok"`
SchemaVersion int `json:"schema_version"`
APISchemaVersion string `json:"api_schema_version"`
Uptime string `json:"uptime"`
DBPath string `json:"db_path"`
}
getAndUnmarshal(t, ts, "/api/v1/health", http.StatusOK, &body)
assert.True(t, body.OK)
assert.Equal(t, db.CurrentSchemaVersion(), body.SchemaVersion)
assert.Equal(t, daemon.APISchemaVersion, body.APISchemaVersion)
assert.NotEmpty(t, body.APISchemaVersion)
assert.NotEmpty(t, body.Uptime)
assert.NotEmpty(t, body.DBPath)
}
Loading