Skip to content

📡 Daemon API: gRPC (ConnectRPC) + REST

📌 Description

The daemon's API server: the same axum router — gRPC (tonic; compatible with the platform's ConnectRPC clients) and REST (JSON over HTTP) — served on two listeners: a TCP port for the platform (bearer-token auth) and a local unix socket for the CLI (peer-credential auth, DMN-042). All transports call one service layer; every call carries the caller's user context, which scopes app visibility (a user sees their own apps, root everyone's). The contracts are protobuf files in proto/, which are also the source of truth for the platform's clients (a buf dependency).

🎯 Scenarios

  • The asc CLI manages apps through the unix socket without sudo and without the docker group: asc install, asc ls, asc app start work for a regular user and touch only that user's apps.
  • The AdminService.Cloud platform manages the node over gRPC/ConnectRPC (through the nodeservice tunnel).
  • Scripts and third-party integrations use REST: curl -H "Authorization: Bearer <token>" http://127.0.0.1:8420/v1/apps, or the socket: curl --unix-socket /run/asc/asc.sock http://localhost/v1/apps (no token — identity comes from the kernel).
  • Before opening a WebSocket console the client requests a temporary console token (IssueConsoleToken) — the platform does this automatically after checking permissions.

🏗️ Technical design

Server

  • TCP listener on 127.0.0.1:8420 (the [api] listen setting in config.toml). The port is not exposed externally — remote access goes through the platform tunnel (DMN-005 → tunnel).
  • Unix-socket listener on /run/asc/asc.sock (the [api] socket setting; the directory comes from the systemd unit's RuntimeDirectory=asc). Best-effort: a host where the socket cannot be bound keeps the TCP API and logs a warning.
  • One router: axum REST + tonic routes for gRPC (h2c), dispatched by path/Content-Type.
  • All blocking operations (docker/systemctl/git) go to spawn_blocking — the event loop is never blocked.

🔐 Authentication and the user context

Every authenticated request gets a UserContext (uid, user name, root flag) stamped by the transport middleware; the service layer enforces app ownership from it — the same rule as the CLI's in-process mode (DMN-002).

TCP (platform): bearer token.

  • Generated by the daemon on first start (32 bytes from a CSPRNG), stored in api.token next to config.toml (root-only, 0600).
  • Required for both transports: REST — the Authorization: Bearer <token> header, gRPC — the authorization metadata. Comparison is constant-time.
  • Without a token: REST → 401 {"error": ...}, gRPC → UNAUTHENTICATED.
  • Token calls act with full visibility (the platform performs its own per-user permission checks); per-user API tokens are a post-MVP task.

Unix socket (CLI): SO_PEERCRED. (DMN-042)

  • No token. The daemon asks the kernel for the connecting process's uid (SO_PEERCRED) and builds the context from it — nothing inside the request can escalate privileges.
  • The socket file is world-connectable (0666) on purpose: reaching it grants nothing, authorization is the peer uid, per request. A regular user sees and manages only their own apps; a root peer sees everyone's.
  • sudo asc ... attribution: the CLI forwards SUDO_UID/SUDO_USER as the X-Asc-Sudo-Uid/X-Asc-Sudo-User headers; the daemon honors them only when the peer itself is root, mirroring the in-process behavior — new apps are attributed to the invoking user while sudo keeps full visibility.
  • CLI routing: the lifecycle commands (ls/status/install/app start|stop|restart|logs|remove|info) go through the socket whenever it exists. No socket file — the CLI works in-process as before (DMN-041: root on the system paths, a user on their private ~/.asc tree). A socket that exists but does not answer is an error for a regular user and a warned in-process fallback for root (recovery must not depend on a healthy daemon).
  • Known limits (DMN-043): docker containers still run with the daemon's (root) privileges — a malicious manifest can request dangerous mounts, so a container policy for non-root owners is the follow-up; private-repo installs through the daemon use the daemon's git credentials, not the caller's; asc attach still opens Docker directly and needs root.

🎫 Temporary console tokens

  • AppService.IssueConsoleToken(app_id, session) / POST /v1/apps/{id}/console-token {"session": "logs"|"attach"}.
  • The token is single-use, TTL 30 seconds, bound to an application and a session type; kept in the daemon's memory.
  • The WebSocket console (DMN-007) accepts connections only with such a token.

🗺️ REST routes ↔ gRPC methods

RESTgRPCDescription
GET /v1/statusDaemonService.GetStatusVersion, application counters
GET /v1/appsAppService.ListAppsApplication list (scoped by the caller's context)
POST /v1/apps {"spec": ..., "source"?, "name"?, "branch"?, "tag"?, "license_ack"?}AppService.InstallAppInstall from a registry or directly from a git URL (DMN-040); without license_ack a repository shipping a LICENSE fails with 409 + license_required payload, an ambiguous package with 409 + ambiguous (candidate list) — the CLI renders its consent prompt / source pick from these
GET /v1/apps/{id}AppService.GetAppA single application
GET /v1/apps/{id}/diskAppService.GetAppDiskDisk usage: image, repository, data, custom volumes
POST /v1/apps/{id}/start|stop|restartAppService.Start/Stop/RestartAppLifecycle
GET /v1/apps/{id}/logs?tail=NAppService.GetAppLogsLog tail
DELETE /v1/apps/{id}AppService.RemoveAppRemoval including data
POST /v1/apps/{id}/console-tokenAppService.IssueConsoleTokenTemporary console token
GET /v1/metricsMonitorService.GetSystemMetricsCurrent system metrics (503 until the first sample)
GET /v1/metrics/history?limit=NMonitorService.GetMetricsHistoryMetrics history from the ring buffer, oldest → newest

📜 Code generation

  • Rust code is generated from proto/ in build.rs via protox (a pure-Rust protobuf compiler) + tonic-build — no system protoc needed, the build is hermetic.
  • Contract changes are backward-compatible only (new fields are optional, field numbers are never reused).

DMN-005, DMN-007, DMN-042, DMN-043 in ROADMAP.md.

Released under the MIT License.