Platform

Platform overviewArchitectureWorkflow orchestrationGitOps configurationGovernance and AAAAI and MCPKnowledge and contextRuntime and executionEvidence and monitoring

Use cases

All use casesProduction incidentRelease preparationHotfix to productionSecurity scan triage
Why NopsAIIntegrationsSecurity

Resources

All resourcesAI agent governanceMCP governanceMCP securitySelf-hosted platforms
PricingGitHub

Company

How a run worksAboutContactBook a demo

System operations API

Runtime configuration, the runner fleet and its install generation, platform logs, mail delivery, and the data lifecycle.

ReferenceAdministratorOperatorSecurity

Key points

  • Runner install generation is separately authorized because it hands out service credentials, and the one-time token expires in 10 minutes and is consumed by the first download.
  • Ejecting a runner blocklists its id; an ordinary disconnect keeps the registration so it can reconnect.
  • Disabling dispatch is the drain switch: new work stops, in-flight runs finish.
  • Log sources are allow-listed, and lines are redacted before they leave the platform.
  • Live log streaming is concurrency-capped, so a forgotten stream answers 429 for the next one.
  • The platform takes and stores backups. It does not upload or restore them.
  • Cleanup is permanent. Preview first — the counts are easy to misread by an order of magnitude.

Operations

GET/v1/system/configAuthorized

Reads effective runtime configuration.

Notes

This is what the platform resolved, not what a file says: environment, config file, and runtime overrides are already merged here.

Call it

Read runtime configurationapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config" | jq
Result

The settings the platform is running with. Secret values are never included.

Responses

200application/json

Effective configuration.

{
  "dispatcher_grpc_address": "dispatcher:9091",
  "runner_capacity": 1,
  "default_pipeline_timeout": "60m"
}

Side effects

  • None.

Proven by

  • services/nopsai/system_services_test.go
  • services/nopsai/runtime_settings_store.go
PUT/v1/system/configAuthorized

Updates runtime configuration.

Notes

Changing the dispatcher address here affects newly generated runner installs, not runners that are already registered.

Call it

Change a runtime settingapi-system-operations request
curl -sX PUT "$NOPSAI_URL/v1/system/config" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"default_pipeline_timeout":"90m"}' | jq
Result

The stored configuration. Services pick it up through the runtime-config watch rather than a restart.

Responses

200application/json

Configuration stored.

{ "default_pipeline_timeout": "90m" }

When it fails

StatusCauseWhat to do
400An unknown setting or an unusable value.The message names the field.
500The configuration could not be stored.Retry.

Side effects

  • Platform services pick the change up through their runtime-config watch.
  • Writes an audit record.

Proven by

  • services/nopsai/system_services_test.go
  • services/nopsai/runtime_settings_store.go
GET/v1/system/dispatcherAuthorized

Reads dispatcher and runner fleet status.

Notes

A run that stays queued is answered here: no reachable, dispatch-enabled runner with a matching scope means nothing can pick it up.

Call it

Check the fleetapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/dispatcher" | jq '.runners'
Result

Registered runners with scopes, capacity, reachability, and whether dispatch is enabled.

Responses

200application/json

Fleet status.

{
  "runners": [
    { "runner_id": "runner-local-1", "scopes": ["prod"], "capacity": 2, "reachable": true, "dispatch_enabled": true }
  ]
}

Side effects

  • None.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/system_handlers.go
GET/v1/system/dispatcher/scopesAuthorized

Lists the runtime scopes runners currently advertise.

Notes

This is the live view derived from registered runners, not the configured route map. A scope missing here has no capacity behind it.

Call it

List dispatchable scopesapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/dispatcher/scopes" | jq
Result

The scopes work can actually be dispatched to right now.

Responses

200application/json

Advertised scopes.

["prod", "ci"]

When it fails

StatusCauseWhat to do
500The scope query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/system_handlers.go
POST/v1/system/dispatcher/runners/{runnerID}/dispatchAuthorized

Enables or disables dispatch to one runner.

Notes

This is the drain switch: disable dispatch, wait for the runner to go idle, then take the host away.

Parameters

NameInTypeRequiredDescription
runnerIDpathstringRequiredRunner identifier as registered with the dispatcher.

Call it

Drain a runnerapi-system-operations request
curl -sX POST "$NOPSAI_URL/v1/system/dispatcher/runners/$RUNNER_ID/dispatch" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}' -w "%{http_code}\n"
Result

204. New work stops being assigned; runs already executing finish.

Responses

204

Dispatch flag updated.

When it fails

StatusCauseWhat to do
400An unknown runner or a malformed body.Send {"enabled": true|false}.

Side effects

  • Stops or resumes new assignments. In-flight runs are unaffected.
  • Writes an audit record.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/system_handlers.go
DELETE/v1/system/dispatcher/runners/{runnerID}Authorized

Ejects a runner from the fleet.

Notes

Ejection is deliberate removal, not a disconnect. An ordinary network drop keeps the registration so the runner can reconnect; ejection blocklists it.

Parameters

NameInTypeRequiredDescription
runnerIDpathstringRequiredRunner identifier as registered with the dispatcher.

Call it

Eject a runnerapi-system-operations request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/dispatcher/runners/$RUNNER_ID" -w "%{http_code}\n"
Result

204. The runner is disconnected and its id added to the ejected list so it cannot rejoin.

Responses

204

Runner ejected.

When it fails

StatusCauseWhat to do
400The runner id is missing or unknown.Take the id from the fleet view.
500The ejection could not be recorded.Retry; the runner may still be connected.

Side effects

  • Disconnects the runner and clears its dispatcher status.
  • Adds its id to ejected_runner_ids, which is what stops it reconnecting.
  • Writes an audit record.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/system_handlers.go
GET/v1/system/dispatcher/runner-bootstrap-commandAuthorized

Generates a one-time Docker runner install command.

Notes

The response warnings are worth reading: they say when the dispatcher address had to be derived from the request, which is the usual cause of a runner that installs and never connects.

Parameters

NameInTypeRequiredDescription
runner_idquerystringOptionalRunner identity to install.Default: runner-prod-1
runner_scopesquerystringOptionalScopes the runner advertises. An explicitly empty value means all scopes.Default: configured default, then `prod`
runner_capacityqueryintegerOptionalConcurrent runs the runner accepts. Must be a positive integer.Default: 1
runner_network_modequerystringOptionalDocker network mode for the generated container.Allowed: bridge, host, autoDefault: auto
runner_imagequerystringOptionalOverride the runner image tag.Default: the platform runner image
registry_credential_refquerystringOptionalCredential whose docker config is delivered for image pulls.

Call it

Generate a runner install commandapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
  "$NOPSAI_URL/v1/system/dispatcher/runner-bootstrap-command?runner_id=runner-local-1&runner_scopes=prod&runner_capacity=2" \
  | jq -r .bootstrap_command
Result

A one-line command whose token expires in 10 minutes and is consumed by the first successful download.

Responses

200application/json

The install command plus the identity and placement it encodes.

{
  "runner_id": "runner-local-1",
  "runner_scopes": "prod",
  "runner_capacity": 2,
  "dispatcher_address": "dispatcher:9091",
  "network_mode": "bridge",
  "runner_image": "ghcr.io/nopsai/nopsai-docker-runner:dev",
  "bootstrap_command": "tmp=$(mktemp) && curl -fsSL -H 'Authorization: Bearer ...' ... && sh \"$tmp\"",
  "expires_at": "2026-08-19T13:22:04Z",
  "warnings": ["This one-time install command expires in 10 minutes and is consumed by the first successful download."]
}

When it fails

StatusCauseWhat to do
400A non-positive capacity, an unknown network mode, or an unusable runner id.runner_network_mode must be bridge, host, or auto.
403The caller may not generate runner installs.Install generation hands out service credentials, so it is separately authorized.
500Required dispatcher TLS or service material is not configured.The message names what is missing, for example DISPATCHER_TLS_SECRET.

Side effects

  • Issues a one-time token that carries the install script.
  • Writes an audit record.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/internal/runnerinstall/docker.go
GET/v1/system/dispatcher/runner-bootstrapPublic

Downloads the install script named by a one-time bootstrap token.

Notes

Public by necessity — the runner host has no platform credentials yet — but bounded by a single-use, short-lived token rather than being open.

Parameters

NameInTypeRequiredDescription
AuthorizationheaderstringRequiredThe one-time bearer token embedded in the generated command.

Call it

What the generated command runsapi-system-operations request
tmp=$(mktemp) && curl -fsSL -H "Authorization: Bearer $ONE_TIME_TOKEN" \
  "$NOPSAI_URL/v1/system/dispatcher/runner-bootstrap" -o "$tmp" && sh "$tmp"
Result

A shell script that pulls the runner image and starts the container. The token is consumed by this download.

Replace before running
  • $ONE_TIME_TOKEN comes from the bootstrap-command response and lasts 10 minutes.

Responses

200text/x-shellscript; charset=utf-8

The install script.

#!/bin/sh
set -eu

if ! command -v docker >/dev/null 2>&1; then
  echo "docker is required on this runner host" >&2
  exit 1
fi

echo "Installing NopsAI runner runner-local-1"
...
container_id=$(docker run -d \
  --name runner-local-1-abc123 \
  --restart always \
  --network nopsai-net \
  -v /var/run/docker.sock:/var/run/docker.sock ...)

When it fails

StatusCauseWhat to do
404The token is unknown, expired, or already used.Generate a new install command. A token works exactly once.
500The script could not be produced.Retry the generation step.

Side effects

  • Consumes the one-time token.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/internal/runnerinstall/docker.go
  • services/nopsai/http_middleware.go
GET/v1/system/dispatcher/runner-composeAuthorized

Generates a Docker Compose service for a runner.

Notes

Use this when the runner belongs in a Compose file you keep. It embeds long-lived material directly, so treat the output as a secret.

Parameters

NameInTypeRequiredDescription
runner_idquerystringOptionalRunner identity.Default: runner-prod-1
runner_scopesquerystringOptionalScopes the runner advertises.
runner_capacityqueryintegerOptionalConcurrent runs accepted.

Call it

Generate a Compose serviceapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
  "$NOPSAI_URL/v1/system/dispatcher/runner-compose?runner_id=runner-local-1" | jq -r .compose
Result

A Compose service fragment plus the command to bring it up.

Responses

200application/json

The Compose fragment and its up command.

{
  "runner_id": "runner-local-1",
  "resource_name": "runner-local-1-abc123",
  "compose": "runner-local-1-abc123:\n  image: ghcr.io/nopsai/nopsai-docker-runner:dev\n  ...",
  "command": "docker compose -f docker-compose.yaml up -d runner-local-1-abc123"
}

When it fails

StatusCauseWhat to do
400An invalid capacity or runner identity.The message names the parameter.
500Required dispatcher material is not configured.The message names what is missing.

Side effects

  • None. Unlike the bootstrap command, this issues no token.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/internal/runnerinstall/docker.go
GET/v1/system/dispatcher/kubernetes-runner-bootstrap-commandAuthorized

Generates a one-time Kubernetes runner install command.

Notes

A cross-namespace runner needs the fully qualified dispatcher service name; check the address in the response before running the command.

Parameters

NameInTypeRequiredDescription
runner_idquerystringOptionalRunner identity.
runner_scopesquerystringOptionalScopes the runner advertises.
namespacequerystringOptionalNamespace the runner is installed into.

Call it

Generate a Kubernetes runner installapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
  "$NOPSAI_URL/v1/system/dispatcher/kubernetes-runner-bootstrap-command?runner_id=runner-k8s-1" \
  | jq -r .bootstrap_command
Result

A one-time command that applies the runner manifest into the cluster.

Responses

200application/json

The install command and the identity it encodes.

{
  "runner_id": "runner-k8s-1",
  "runner_scopes": "prod",
  "bootstrap_command": "tmp=$(mktemp) && curl -fsSL -H 'Authorization: Bearer ...' ... && sh \"$tmp\"",
  "expires_at": "2026-08-19T13:22:04Z"
}

When it fails

StatusCauseWhat to do
400An invalid runner identity or namespace.The message names the parameter.
403The caller may not generate runner installs.Separately authorized because it hands out service credentials.
500Required dispatcher material is not configured.The message names what is missing.

Side effects

  • Issues a one-time token.
  • Writes an audit record.

Proven by

  • services/nopsai/config_sync_test.go
  • services/nopsai/internal/runnerinstall/docker.go
  • doc/kubernetes-runner.md
GET/v1/system/dispatcher/kubernetes-runner-manifestAuthorized

Generates a Kubernetes runner manifest.

Notes

Committing this manifest commits runner credentials. Prefer the one-time bootstrap command unless the manifest is going into a sealed-secret workflow.

Parameters

NameInTypeRequiredDescription
runner_idquerystringOptionalRunner identity.
namespacequerystringOptionalNamespace to render into.

Call it

Render a runner manifest for GitOpsapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
  "$NOPSAI_URL/v1/system/dispatcher/kubernetes-runner-manifest?runner_id=runner-k8s-1" | jq -r .manifest
Result

The manifest to commit or apply. It carries runner credentials, so treat it as a secret.

Responses

200application/json

The rendered manifest.

{
  "runner_id": "runner-k8s-1",
  "namespace": "nopsai-runners",
  "manifest": "apiVersion: apps/v1\nkind: Deployment\n..."
}

When it fails

StatusCauseWhat to do
400An invalid runner identity or namespace.The message names the parameter.
500Required dispatcher material is not configured.The message names what is missing.

Side effects

  • None. No token is issued.

Proven by

  • services/nopsai/config_sync_test.go
  • doc/kubernetes-runner.md
GET/v1/system/logs/sourcesAuthenticated

Lists allow-listed platform log sources.

Notes

The allow list is the point: the platform reads its own services through a socket proxy rather than being handed a writable Docker socket.

Call it

List log sourcesapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/logs/sources" | jq '.[] | {id, name}'
Result

One entry per platform service the provider exposes. Non-platform containers are not sources.

Responses

200application/json

Available log sources.

[
  { "id": "nopsai", "name": "nopsai" },
  { "id": "dispatcher", "name": "dispatcher" }
]

When it fails

StatusCauseWhat to do
502The log provider could not be reached.On Docker installs this is usually the socket proxy; on Kubernetes it is the API server.
503System logs are not configured for this install.Configure a Docker or Kubernetes log provider.

Side effects

  • None.

Proven by

  • services/nopsai/system_services_test.go
  • doc/system-logs.md
GET/v1/system/logs/sources/{sourceID}/tailAuthenticated

Returns the recent log buffer for one source.

Notes

Secret values are masked before the lines leave the platform, so a tail is safe to paste into an incident channel.

Parameters

NameInTypeRequiredDescription
sourceIDpathstringRequiredSource id from the source list.
limitqueryintegerOptionalHow many lines to return.

Call it

Tail a serviceapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/logs/sources/dispatcher/tail?limit=200"
Result

Recent lines, already redacted. Usually enough to answer "did this service start?".

Responses

200application/json

Recent log lines.

[
  { "timestamp": "2026-08-19T08:11:04Z", "line": "dispatcher listening on :9090" }
]

When it fails

StatusCauseWhat to do
400An unknown source or an invalid limit.Take the id from the source list.
503System logs are not configured.Configure a log provider.

Side effects

  • None.

Proven by

  • services/nopsai/system_services_test.go
  • doc/system-logs.md
GET/v1/system/logs/sources/{sourceID}/streamAuthenticated

Streams live logs for one source.

Notes

The 429 is a concurrency cap rather than a rate limit: streams are expensive, so a forgotten open stream blocks the next one.

Parameters

NameInTypeRequiredDescription
sourceIDpathstringRequiredSource id from the source list.

Call it

Follow a service while reproducingapi-system-operations request
curl -N -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/logs/sources/dispatcher/stream"
Result

Recent lines replayed, then live output as it happens.

Responses

200text/event-stream

A server-sent event stream of log lines.

data: {"timestamp":"2026-08-19T08:11:04Z","line":"dispatcher listening on :9090"}

data: {"timestamp":"2026-08-19T08:11:07Z","line":"runner runner-local-1 registered"}

When it fails

StatusCauseWhat to do
400An unknown source.Take the id from the source list.
429Too many concurrent streams.Close an existing stream; live streaming is deliberately bounded.
503System logs are not configured.Configure a log provider.

Streaming

text/event-stream - Server-sent events; recent lines are replayed before live output begins.

Side effects

  • Holds an open connection against the stream limit until closed.

Proven by

  • services/nopsai/system_services_test.go
  • doc/system-logs.md
GET/v1/system/notifications/mailAuthorized

Reads platform mail settings.

Notes

Delivery is configured once here; which events a team hears about is configured per team.

Call it

Read mail settingsapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/notifications/mail" | jq
Result

Host, port, sender, and the credential reference. The credential value is not returned.

Responses

200application/json

Mail settings.

{
  "host": "smtp.example.com",
  "port": 587,
  "sender": "[email protected]",
  "credential_ref": "platform/smtp"
}

When it fails

StatusCauseWhat to do
500Settings could not be loaded.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/system_services_test.go
  • services/nopsai/system_handlers.go
PUT/v1/system/notifications/mailAuthorized

Updates platform mail settings.

Call it

Configure SMTPapi-system-operations request
curl -sX PUT "$NOPSAI_URL/v1/system/notifications/mail" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"host":"smtp.example.com","port":587,"sender":"[email protected]","credential_ref":"platform/smtp"}' | jq
Result

The stored settings. Storing them does not prove they work — send a test.

Responses

200application/json

Settings stored.

{
  "host": "smtp.example.com",
  "port": 587,
  "sender": "[email protected]"
}

When it fails

StatusCauseWhat to do
400A missing host, an invalid port, or an unresolvable credential reference.The message names the field.
500Settings could not be stored.Retry.

Side effects

  • Changes how every notification is delivered.
  • Writes an audit record.

Proven by

  • services/nopsai/system_services_test.go
  • services/nopsai/system_handlers.go
POST/v1/system/notifications/mail/testAuthorized

Sends a test message through the configured mail settings.

Notes

A team subscription with broken delivery looks exactly like no subscription. Test after every mail change.

Call it

Send a test messageapi-system-operations request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/notifications/mail/test" | jq
Result

A real delivery attempt, which is the only way to know the credentials and sender are accepted.

Responses

200application/json

The message was accepted by the server.

{ "ok": true }

When it fails

StatusCauseWhat to do
400Mail settings are incomplete.Configure the host, port, and sender first.
502The mail server rejected the message or could not be reached.The message carries the server response — usually authentication or a sender the domain will not accept.
500The test could not run.Platform fault.

Side effects

  • Sends a real email.

Proven by

  • services/nopsai/system_services_test.go
  • services/nopsai/system_handlers.go
GET/v1/system/data/backupsAuthorized

Lists database backups the platform has taken.

Notes

The platform takes and stores backups. It does not upload them anywhere or restore them — that stays an operator responsibility.

Call it

List backupsapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/backups" | jq '.[] | {id, status, size_bytes, created_at}'
Result

Each backup with its status, size, and checksum.

Responses

200application/json

Backups.

[{
  "id": "b7f2c910-8d43-4a26-9c05-31e7a4b0d986",
  "backup_type": "database",
  "status": "complete",
  "file_name": "nopsai-2026-08-19.dump",
  "content_type": "application/octet-stream",
  "size_bytes": 48213004,
  "checksum_sha256": "9f2c1b...",
  "created_at": "2026-08-19T02:00:11Z"
}]

When it fails

StatusCauseWhat to do
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/backupsAuthorized

Takes a backup.

Notes

A backup that never leaves the platform volume does not survive losing the platform. Download it.

Call it

Take a backup before a risky changeapi-system-operations request
curl -sX POST "$NOPSAI_URL/v1/system/data/backups" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"backup_type":"database"}' | jq -r .id
Result

The backup record. Download it somewhere off the platform to make it a real backup.

Responses

201application/json

Backup started.

{
  "id": "b7f2c910-8d43-4a26-9c05-31e7a4b0d986",
  "backup_type": "database",
  "status": "complete",
  "file_name": "nopsai-2026-08-19.dump",
  "content_type": "application/octet-stream",
  "size_bytes": 48213004,
  "checksum_sha256": "9f2c1b...",
  "created_at": "2026-08-19T02:00:11Z"
}

When it fails

StatusCauseWhat to do
400An unknown backup type.The message names the accepted values.
500The backup could not be taken.Check disk space on the platform volume.

Side effects

  • Writes a dump to the platform volume, consuming disk.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
GET/v1/system/data/backups/{backupID}/downloadAuthorized

Downloads a backup file.

Notes

Verify the checksum after downloading. An unverified backup is a hope, not a restore path.

Parameters

NameInTypeRequiredDescription
backupIDpathuuidRequiredBackup identifier.

Call it

Download a backup and verify itapi-system-operations request
curl -s -OJ -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/backups/$BACKUP_ID/download"
shasum -a 256 nopsai-2026-08-19.dump
Result

The dump file. Compare the digest with checksum_sha256 from the backup record.

Responses

200application/octet-stream

The backup file.

When it fails

StatusCauseWhat to do
404No backup with that id.List the backups.
409The backup is still running or failed.Wait for status: complete.
500The file could not be read.The dump may have been removed from the volume.

Side effects

  • None.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
DELETE/v1/system/data/backups/{backupID}Authorized

Deletes a backup.

Parameters

NameInTypeRequiredDescription
backupIDpathuuidRequiredBackup to delete.

Call it

Delete a backupapi-system-operations request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/backups/$BACKUP_ID" -w "%{http_code}\n"
Result

204, freeing the disk it occupied.

Responses

204

Backup deleted.

When it fails

StatusCauseWhat to do
404No backup with that id.It may already be deleted.
500The delete failed.Retry.

Side effects

  • Removes the dump from the platform volume permanently.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/previewAuthorized

Reports what a cleanup would remove, without removing it.

Notes

Always preview first. Cleanup is permanent and has no undo beyond a restore.

Call it

Preview a cleanupapi-system-operations request
curl -sX POST "$NOPSAI_URL/v1/system/data/cleanup/preview" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"retention_days":90}' | jq
Result

Counts per record type. Nothing is deleted.

Responses

200application/json

What would be removed.

{
  "retention_days": 90,
  "would_remove": { "pipeline_runs": 1240, "logs": 98211 }
}

When it fails

StatusCauseWhat to do
400An invalid retention window.Retention is in whole days.
500The preview query failed.Platform fault.

Side effects

  • None. A preview never deletes.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/runAuthorized

Runs a cleanup now.

Notes

Take a backup before the first cleanup on an install. The counts in a preview are easy to misread by an order of magnitude.

Call it

Run a cleanupapi-system-operations request
curl -sX POST "$NOPSAI_URL/v1/system/data/cleanup/run" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"retention_days":90}' | jq -r .id
Result

A job record. Read the job to see what it removed.

Responses

201application/json

Cleanup job created.

{
  "id": "3a19d740-6c82-4f15-b0e3-8d5c2a9f4711",
  "status": "complete",
  "removed": { "pipeline_runs": 1240, "logs": 98211 },
  "started_at": "2026-08-19T03:00:00Z",
  "finished_at": "2026-08-19T03:04:22Z"
}

When it fails

StatusCauseWhat to do
400An invalid retention window.Preview the same window first.
500The cleanup could not be started.Retry.

Side effects

  • Permanently deletes run records, logs, and outputs older than the window.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
GET/v1/system/data/cleanup/jobsAuthorized

Lists cleanup jobs and what they removed.

Notes

This is the record of what a retention policy has actually cost you in history.

Parameters

NameInTypeRequiredDescription
limitqueryintegerOptionalHow many jobs to return.

Call it

Read cleanup historyapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/jobs" | jq
Result

Each job with its status and per-type removal counts.

Responses

200application/json

Cleanup jobs.

[{
  "id": "3a19d740-6c82-4f15-b0e3-8d5c2a9f4711",
  "status": "complete",
  "removed": { "pipeline_runs": 1240, "logs": 98211 },
  "started_at": "2026-08-19T03:00:00Z",
  "finished_at": "2026-08-19T03:04:22Z"
}]

When it fails

StatusCauseWhat to do
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
GET/v1/system/data/cleanup/schedulesAuthorized

Lists cleanup schedules.

Call it

List cleanup schedulesapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/schedules" | jq
Result

Each schedule with its cron expression, timezone, and retention window.

Responses

200application/json

Cleanup schedules.

[{
  "id": "5e8c31a0-9b47-4d12-8f60-2a7d4c1e9b03",
  "cron_expression": "0 3 * * *",
  "timezone": "Europe/Berlin",
  "enabled": true,
  "retention_days": 90
}]

When it fails

StatusCauseWhat to do
500The query failed.Platform fault.

Side effects

  • None.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/schedulesAuthorized

Creates a cleanup schedule.

Notes

Preview the same retention window by hand before scheduling it. A schedule turns a one-off mistake into a nightly one.

Call it

Automate retentionapi-system-operations request
curl -sX POST "$NOPSAI_URL/v1/system/data/cleanup/schedules" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cron_expression":"0 3 * * *","timezone":"Europe/Berlin","retention_days":90}' | jq -r .id
Result

The schedule id. Retention becomes a setting rather than a chore.

Responses

201application/json

Schedule created.

{
  "id": "5e8c31a0-9b47-4d12-8f60-2a7d4c1e9b03",
  "cron_expression": "0 3 * * *",
  "timezone": "Europe/Berlin",
  "enabled": true,
  "retention_days": 90
}

When it fails

StatusCauseWhat to do
400An invalid cron expression, timezone, or retention window.The message names the field.
500The schedule could not be created.Retry.

Side effects

  • Every firing permanently deletes records older than the window.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
PUT/v1/system/data/cleanup/schedules/{scheduleID}Authorized

Replaces a cleanup schedule.

Parameters

NameInTypeRequiredDescription
scheduleIDpathstringRequiredCleanup schedule identifier.

Call it

Change the retention windowapi-system-operations request
curl -sX PUT "$NOPSAI_URL/v1/system/data/cleanup/schedules/$SCHEDULE_ID" \
  -H "Authorization: Bearer $NOPSAI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cron_expression":"0 3 * * *","timezone":"Europe/Berlin","retention_days":180}' | jq
Result

The stored schedule. A longer window keeps more history; a shorter one deletes more on the next firing.

Responses

200application/json

Schedule replaced.

{
  "id": "5e8c31a0-9b47-4d12-8f60-2a7d4c1e9b03",
  "cron_expression": "0 3 * * *",
  "timezone": "Europe/Berlin",
  "enabled": true,
  "retention_days": 90
}

When it fails

StatusCauseWhat to do
400An invalid expression or window.The message names the field.
404No schedule with that id.List the schedules.
500The update failed.Retry.

Side effects

  • Shortening the window means the next firing deletes more.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/schedules/{scheduleID}/runAuthorized

Runs a cleanup schedule now.

Parameters

NameInTypeRequiredDescription
scheduleIDpathstringRequiredCleanup schedule identifier.

Call it

Run a cleanup schedule immediatelyapi-system-operations request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/schedules/$SCHEDULE_ID/run" | jq
Result

202, using the schedule own retention window. The next firing is unchanged.

Responses

202application/json

Cleanup started from the schedule.

{
  "id": "3a19d740-6c82-4f15-b0e3-8d5c2a9f4711",
  "status": "complete",
  "removed": { "pipeline_runs": 1240, "logs": 98211 },
  "started_at": "2026-08-19T03:00:00Z",
  "finished_at": "2026-08-19T03:04:22Z"
}

When it fails

StatusCauseWhat to do
404No schedule with that id.List the schedules.
500The cleanup could not be started.Retry.

Side effects

  • Permanently deletes records older than the schedule window.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/schedules/{scheduleID}/enableAdministrator

Enables a cleanup schedule.

Parameters

NameInTypeRequiredDescription
scheduleIDpathstringRequiredCleanup schedule identifier.

Call it

Enable a cleanup scheduleapi-system-operations request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/schedules/$SCHEDULE_ID/enable" | jq '{id, enabled}'
Result

Firing resumes from now.

Responses

200application/json

Schedule enabled.

{
  "id": "5e8c31a0-9b47-4d12-8f60-2a7d4c1e9b03",
  "cron_expression": "0 3 * * *",
  "timezone": "Europe/Berlin",
  "enabled": true,
  "retention_days": 90
}

When it fails

StatusCauseWhat to do
404No schedule with that id.List the schedules.
500The change failed.Retry.

Side effects

  • Resumes automatic deletion on the schedule.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
POST/v1/system/data/cleanup/schedules/{scheduleID}/disableAdministrator

Disables a cleanup schedule.

Notes

The first thing to disable when an incident needs history that retention is about to delete.

Parameters

NameInTypeRequiredDescription
scheduleIDpathstringRequiredCleanup schedule identifier.

Call it

Pause automatic cleanupapi-system-operations request
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/schedules/$SCHEDULE_ID/disable" | jq '{id, enabled}'
Result

Automatic deletion stops while an investigation needs the history.

Responses

200application/json

Schedule disabled.

{
  "id": "5e8c31a0-9b47-4d12-8f60-2a7d4c1e9b03",
  "cron_expression": "0 3 * * *",
  "timezone": "Europe/Berlin",
  "enabled": true,
  "retention_days": 90
}

When it fails

StatusCauseWhat to do
404No schedule with that id.List the schedules.
500The change failed.Retry.

Side effects

  • Stops automatic deletion. Disk use grows until it is re-enabled.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
DELETE/v1/system/data/cleanup/schedules/{scheduleID}Authorized

Deletes a cleanup schedule.

Parameters

NameInTypeRequiredDescription
scheduleIDpathstringRequiredCleanup schedule identifier.

Call it

Delete a cleanup scheduleapi-system-operations request
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/data/cleanup/schedules/$SCHEDULE_ID" -w "%{http_code}\n"
Result

204. Job history stays.

Responses

204

Schedule deleted.

When it fails

StatusCauseWhat to do
400The id is malformed.Use an id from the list.
404No schedule with that id.It may already be deleted.
500The delete failed.Retry.

Side effects

  • Stops automatic deletion permanently. Cleanup job history remains.
  • Writes an audit record.

Proven by

  • services/nopsai/data_management_gitops_test.go
  • services/nopsai/data_management.go
GET/v1/system/licenseAuthenticated

What this installation is entitled to run, and why.

Notes

Reports no key material, only what the installation may run. A limit of 0 means unlimited, so an omitted limit never reads as forbidding everything. Verification is a local Ed25519 check: NopsAI never calls home.

Call it

Check the entitlementapi-system-operations request
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/license" | jq '{licensed, tier, limits, usage, reason}'
Result

Without a commercial key, licensed: false on the noncommercial tier, with no limits and a reason stating the licence position.

Responses

200application/json

Entitlement, limits, current usage against them, and the reason for any non-licensed state.

{
  "licensed": false,
  "tier": "noncommercial",
  "reason": "No commercial licence key is configured. Running under the non-commercial licence, which is free and uncapped for any non-commercial purpose.",
  "limits": { "max_users": 0, "max_teams": 0, "max_concurrent_runs": 0 },
  "usage": { "users": 1, "teams": 0 }
}

Side effects

  • None. The entitlement is resolved from configuration on each call and cached nowhere.

Proven by

  • pkg/license/license_test.go
  • services/nopsai/system_license.go
  • pkg/license/entitlement.go

How it works

The runner routes come in two shapes for a reason. The one-time bootstrap command hands the host a short-lived token and nothing else, while the Compose fragment and Kubernetes manifest embed long-lived material directly — which makes them convenient for GitOps and dangerous to commit unsealed. Prefer the bootstrap command unless the output is going somewhere encrypted.

Backups and cleanup pull in opposite directions and are easy to get wrong together. A backup that never leaves the platform volume does not survive losing the platform, and a cleanup schedule quietly deletes the history an incident may need. Verify a downloaded backup against its checksum, and disable the cleanup schedule before it removes evidence you are still reading.

Runtime configuration is what the platform resolved rather than what a file says: environment, config file, and runtime overrides are already merged. Changing the dispatcher address affects newly generated installs, not runners that are already registered.

Implementation evidence

  • services/nopsai/system_handlers.go

    Configuration, dispatcher, and notification handlers.

  • services/nopsai/internal/runnerinstall/docker.go

    Runner install generation and one-time token behaviour.

  • services/nopsai/data_management.go

    Backups, cleanup jobs, and cleanup schedules.