📡 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
ascCLI manages apps through the unix socket without sudo and without the docker group:asc install,asc ls,asc app startwork 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] listensetting 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] socketsetting; the directory comes from the systemd unit'sRuntimeDirectory=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.tokennext to config.toml (root-only, 0600). - Required for both transports: REST — the
Authorization: Bearer <token>header, gRPC — theauthorizationmetadata. 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 forwardsSUDO_UID/SUDO_USERas theX-Asc-Sudo-Uid/X-Asc-Sudo-Userheaders; 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~/.asctree). 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 attachstill 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
| REST | gRPC | Description |
|---|---|---|
GET /v1/status | DaemonService.GetStatus | Version, application counters |
GET /v1/apps | AppService.ListApps | Application list (scoped by the caller's context) |
POST /v1/apps {"spec": ..., "source"?, "name"?, "branch"?, "tag"?, "license_ack"?} | AppService.InstallApp | Install 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.GetApp | A single application |
GET /v1/apps/{id}/disk | AppService.GetAppDisk | Disk usage: image, repository, data, custom volumes |
POST /v1/apps/{id}/start|stop|restart | AppService.Start/Stop/RestartApp | Lifecycle |
GET /v1/apps/{id}/logs?tail=N | AppService.GetAppLogs | Log tail |
DELETE /v1/apps/{id} | AppService.RemoveApp | Removal including data |
POST /v1/apps/{id}/console-token | AppService.IssueConsoleToken | Temporary console token |
GET /v1/metrics | MonitorService.GetSystemMetrics | Current system metrics (503 until the first sample) |
GET /v1/metrics/history?limit=N | MonitorService.GetMetricsHistory | Metrics 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 systemprotocneeded, the build is hermetic. - Contract changes are backward-compatible only (new fields are optional, field numbers are never reused).
🔗 Related tasks
DMN-005, DMN-007, DMN-042, DMN-043 in ROADMAP.md.