Skip to content

Add daemon node registration#76

Merged
wauputr4 merged 3 commits into
mainfrom
agent/dimas/c4f3e5d5
Jun 7, 2026
Merged

Add daemon node registration#76
wauputr4 merged 3 commits into
mainfrom
agent/dimas/c4f3e5d5

Conversation

@wauputr4

@wauputr4 wauputr4 commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

  • add daemon_nodes storage migration and shared schema record
  • add admin daemon node create/list/revoke APIs with one-time raw token response and hashed token storage
  • add daemon bearer auth for /daemon/register and /daemon/ping, including last_seen/status updates and revoked-node rejection
  • add mizan-daemon register command that reads daemon_token_path and posts host identity to the control plane

Testing

  • git diff --check
  • not run: cargo/cargo fmt unavailable in this container (cargo is not on PATH)

Co-authored-by: multica-agent <github@multica.ai>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/mizan-api/src/daemon_nodes.rs Outdated
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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.

Suggested change
let token = format!("{}{}", DAEMON_TOKEN_PREFIX, Uuid::now_v7());
let token = format!("{}{}", DAEMON_TOKEN_PREFIX, Uuid::new_v4());

Comment on lines +366 to +373
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()))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Postgres, the INTEGER column type maps to i32 in Rust. Decoding revoked as i64 in list_daemon_nodes will cause a runtime decoding error on Postgres. Changing revoked to i32 in DbDaemonNode makes it compatible with both SQLite and Postgres.

Suggested change
revoked: i64,
revoked: i32,

Option<String>,
Option<String>,
String,
i64,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Postgres, the INTEGER column type maps to i32 in Rust. Decoding revoked as i64 in list_daemon_nodes will cause a runtime decoding error on Postgres. Changing revoked to i32 in the query_as tuple makes it compatible with both SQLite and Postgres.

Suggested change
i64,
i32,

hostname: row.hostname,
public_key: row.public_key,
status: row.status,
revoked: is_enabled(row.revoked),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since row.revoked is changed to i32 for Postgres compatibility, we can directly check row.revoked != 0 instead of using is_enabled(row.revoked) which expects i64.

Suggested change
revoked: is_enabled(row.revoked),
revoked: row.revoked != 0,

Comment on lines +1 to +14
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
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
);

Comment thread crates/mizan-daemon/src/main.rs Outdated
Comment on lines +97 to +109
let response = reqwest::Client::new()
.post(&registration_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}")))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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(&registration_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}")))?;

Dimas and others added 2 commits June 7, 2026 08:04
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
@wauputr4 wauputr4 merged commit e96fd3b into main Jun 7, 2026
1 check passed
@wauputr4 wauputr4 deleted the agent/dimas/c4f3e5d5 branch June 7, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant