Add daemon node registration#76
Conversation
Co-authored-by: multica-agent <github@multica.ai>
There was a problem hiding this comment.
Code Review
This pull request introduces daemon node management and registration capabilities, adding endpoints for listing, creating, revoking, registering, and pinging daemon nodes, along with a new database migration and a daemon registration CLI command. The review feedback highlights several critical improvements: replacing predictable Uuid::now_v7() with Uuid::new_v4() for secret tokens to enhance security, fixing multiple SQLite-specific queries and schema definitions (such as integer decoding and foreign key types) to ensure Postgres compatibility, and configuring a timeout on the daemon's HTTP client to prevent indefinite hangs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let hostname = normalize_optional(payload.hostname); | ||
| let public_key = normalize_optional(payload.public_key); | ||
| let id = Uuid::now_v7(); | ||
| let token = format!("{}{}", DAEMON_TOKEN_PREFIX, Uuid::now_v7()); |
There was a problem hiding this comment.
Using Uuid::now_v7() for a secret token is a security risk. UUID v7 is time-based and sequential, making it highly predictable, especially since the public node id is also a UUID v7 generated at almost the exact same millisecond. This significantly reduces the entropy and security of the daemon token. Please use Uuid::new_v4() (which is fully random) to ensure maximum entropy and prevent predictability.
| let token = format!("{}{}", DAEMON_TOKEN_PREFIX, Uuid::now_v7()); | |
| let token = format!("{}{}", DAEMON_TOKEN_PREFIX, Uuid::new_v4()); |
| let exists = query_as::<_, (i64,)>(&prepare_sql( | ||
| database_backend, | ||
| "SELECT 1 FROM users WHERE id = ?", | ||
| )) | ||
| .bind(user_id.to_string()) | ||
| .fetch_optional(database) | ||
| .await | ||
| .map_err(|error| AppError::infrastructure(error.to_string()))?; |
There was a problem hiding this comment.
In Postgres, SELECT 1 returns an integer (which maps to i32 in Rust). Attempting to decode it into i64 via query_as::<_, (i64,)> will result in a runtime decoding error (type INTEGER is not compatible with i64), causing the endpoint to return a 500 Internal Server Error on Postgres. Since we only care about the existence of the row, we should use query instead of query_as and check fetch_optional without decoding any columns.
| let exists = query_as::<_, (i64,)>(&prepare_sql( | |
| database_backend, | |
| "SELECT 1 FROM users WHERE id = ?", | |
| )) | |
| .bind(user_id.to_string()) | |
| .fetch_optional(database) | |
| .await | |
| .map_err(|error| AppError::infrastructure(error.to_string()))?; | |
| let exists = query(&prepare_sql( | |
| database_backend, | |
| "SELECT 1 FROM users WHERE id = ?", | |
| )) | |
| .bind(user_id.to_string()) | |
| .fetch_optional(database) | |
| .await | |
| .map_err(|error| AppError::infrastructure(error.to_string()))?; |
| hostname: Option<String>, | ||
| public_key: Option<String>, | ||
| status: String, | ||
| revoked: i64, |
There was a problem hiding this comment.
| Option<String>, | ||
| Option<String>, | ||
| String, | ||
| i64, |
There was a problem hiding this comment.
| hostname: row.hostname, | ||
| public_key: row.public_key, | ||
| status: row.status, | ||
| revoked: is_enabled(row.revoked), |
| CREATE TABLE IF NOT EXISTS daemon_nodes ( | ||
| id TEXT PRIMARY KEY, | ||
| host_user_id TEXT, | ||
| label TEXT, | ||
| hostname TEXT, | ||
| public_key TEXT, | ||
| token_hash TEXT NOT NULL UNIQUE, | ||
| status TEXT NOT NULL DEFAULT 'pending', | ||
| revoked INTEGER NOT NULL DEFAULT 0, | ||
| last_seen_at TEXT, | ||
| created_at TEXT NOT NULL, | ||
| updated_at TEXT NOT NULL, | ||
| FOREIGN KEY (host_user_id) REFERENCES users (id) ON DELETE SET NULL | ||
| ); |
There was a problem hiding this comment.
In Postgres, the users.id column is likely of type UUID. Defining host_user_id as TEXT in daemon_nodes and creating a foreign key constraint referencing users(id) will fail on Postgres because foreign keys must be of the exact same type. To support both SQLite and Postgres in a single migration file, use UUID instead of TEXT for UUID columns (SQLite accepts UUID as a type name and treats it as having no affinity, which is perfectly fine).
CREATE TABLE IF NOT EXISTS daemon_nodes (
id UUID PRIMARY KEY,
host_user_id UUID,
label TEXT,
hostname TEXT,
public_key TEXT,
token_hash TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'pending',
revoked INTEGER NOT NULL DEFAULT 0,
last_seen_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (host_user_id) REFERENCES users (id) ON DELETE SET NULL
);| let response = reqwest::Client::new() | ||
| .post(®istration_url) | ||
| .bearer_auth(token) | ||
| .json(&DaemonRegistrationRequest { | ||
| hostname: std::env::var("HOSTNAME") | ||
| .ok() | ||
| .map(|value| value.trim().to_owned()) | ||
| .filter(|value| !value.is_empty()), | ||
| public_key: None, | ||
| }) | ||
| .send() | ||
| .await | ||
| .map_err(|error| AppError::infrastructure(format!("daemon registration failed: {error}")))?; |
There was a problem hiding this comment.
The reqwest::Client is created without a timeout, which means network requests can hang indefinitely if the control plane is unresponsive. It is a best practice to always configure a reasonable timeout (e.g., 10 seconds) for HTTP clients.
let response = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|error| AppError::infrastructure(format!("failed to build http client: {error}")))?
.post(®istration_url)
.bearer_auth(token)
.json(&DaemonRegistrationRequest {
hostname: std::env::var("HOSTNAME")
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty()),
public_key: None,
})
.send()
.await
.map_err(|error| AppError::infrastructure(format!("daemon registration failed: {error}")))?;Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Summary
Testing