Closed
Conversation
Improved readme
Contributor
There was a problem hiding this comment.
Pull request overview
Adds agent self-registration support so a Status Panel instance can register itself with the Stacker Server (via CLI and a local API endpoint), and persists the returned registration data for later use.
Changes:
- Introduce a new
status registerCLI subcommand to register an agent and optionally save/etc/status-panel/registration.json. - Add
POST /api/v1/registerto the local Axum API to trigger agent self-registration from the web/UI or install flow. - Add
agent::registrationmodule to collect a server fingerprint, call the remote registration API, and save the registration result.
Reviewed changes
Copilot reviewed 20 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.rs | Adds Register CLI subcommand and wiring to registration module + persistence. |
| src/comms/local_api.rs | Adds POST /api/v1/register route and handler to perform registration and save locally. |
| src/agent/registration.rs | New module implementing fingerprint collection, remote registration request, and saving registration JSON. |
| src/agent/mod.rs | Exposes the new registration module. |
| TODO.md | Adds roadmap items for marketplace integration and registration flow. |
| README.md | Refreshes project overview/docs and highlights capabilities. |
Comments suppressed due to low confidence (4)
src/comms/local_api.rs:1584
- The request payload allows an arbitrary
serverURL override, and the handler uses it directly to build an outboundreqwestrequest. Because this endpoint is unauthenticated, this becomes an SSRF primitive (POST to attacker-chosen host/port) and can also exfiltrate the machine fingerprint. Consider removing the override from the HTTP API (keep it for the CLI if needed), or strictly validate/allowlist the host + enforcehttpsbefore making the request.
async fn commands_wait(
State(state): State<SharedState>,
Path(_hash): Path<String>,
Query(params): Query<WaitParams>,
headers: HeaderMap,
) -> impl IntoResponse {
if let Err(resp) = validate_agent_id(&headers) {
return resp.into_response();
}
// Optional signing for GET /wait (empty body) controlled by env flag
let require_sig = std::env::var("WAIT_REQUIRE_SIGNATURE")
.map(|v| v == "true")
.unwrap_or(false);
if require_sig {
if let Err(resp) = verify_stacker_post(&state, &headers, &[], "commands:wait").await {
return resp.into_response();
}
} else {
// Lightweight rate limiting without signature
src/comms/local_api.rs:1604
register_handlerperforms synchronous disk I/O viasave_registration()from within an async Axum handler. This can block the Tokio runtime under load. Consider switchingsave_registrationtotokio::fs(async) or calling the existing sync function insidetokio::task::spawn_blocking, and returning a warning in the response if persistence fails (so callers can detect partial success).
headers
.get("X-Agent-Id")
.and_then(|v| v.to_str().ok())
.unwrap_or(""),
)
.await
{
state.audit.rate_limited(
headers
.get("X-Agent-Id")
.and_then(|v| v.to_str().ok())
.unwrap_or(""),
None,
);
return (
StatusCode::TOO_MANY_REQUESTS,
Json(json!({"error": "rate limited"})),
src/comms/local_api.rs:918
- A new HTTP endpoint (
POST /api/v1/register) is added, but there are existing route integration tests intests/http_routes.rscovering most endpoints. Consider adding tests for this route (at least validating expected status codes and any access restrictions you implement, e.g., loopback-only / already-registered behavior), so regressions don’t silently expose the endpoint.
async fn capabilities_handler(State(state): State<SharedState>) -> impl IntoResponse {
let compose_agent_env = std::env::var("COMPOSE_AGENT_ENABLED")
.ok()
.and_then(|v| v.parse::<bool>().ok());
let compose_agent = compose_agent_env.unwrap_or(state.config.compose_agent_enabled);
src/comms/local_api.rs:918
POST /api/v1/registeris exposed on the same router that binds to0.0.0.0(seeserve()), but the handler currently has no authentication/authorization and no restriction to loopback clients. This allows any network-reachable caller to trigger registration attempts and overwrite/etc/status-panel/registration.json. Consider requiring an authenticated session (whenwith_ui), or at minimum rejecting non-loopbackConnectInfo/ClientIpand refusing to run if already registered unless an explicitforceflag is provided.
async fn metrics_ws_stream(state: SharedState, mut socket: WebSocket) {
let mut rx = state.metrics_tx.subscribe();
// Send latest snapshot immediately
let current = state.metrics_store.read().await.clone();
if let Ok(text) = serde_json::to_string(¤t) {
let _ = socket.send(Message::Text(text.into())).await;
}
while let Ok(snapshot) = rx.recv().await {
if let Ok(text) = serde_json::to_string(&snapshot) {
if socket.send(Message::Text(text.into())).await.is_err() {
break;
}
}
}
}
async fn capabilities_handler(State(state): State<SharedState>) -> impl IntoResponse {
let compose_agent_env = std::env::var("COMPOSE_AGENT_ENABLED")
.ok()
.and_then(|v| v.parse::<bool>().ok());
let compose_agent = compose_agent_env.unwrap_or(state.config.compose_agent_enabled);
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+29
to
+88
| pub deployment_id: String, | ||
| pub stack_name: String, | ||
| pub status: String, | ||
| #[serde(default)] | ||
| pub created_at: Option<String>, | ||
| #[serde(default)] | ||
| pub server_ip: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize)] | ||
| pub struct LinkAgentRequest { | ||
| pub session_token: String, | ||
| pub deployment_id: String, | ||
| pub server_fingerprint: ServerFingerprint, | ||
| } | ||
|
|
||
| #[derive(Debug, Serialize)] | ||
| pub struct ServerFingerprint { | ||
| pub hostname: String, | ||
| pub os: String, | ||
| pub cpu_count: u32, | ||
| pub ram_mb: u64, | ||
| pub disk_gb: u64, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone, Deserialize, Serialize)] | ||
| pub struct RegistrationResponse { | ||
| pub agent_id: String, | ||
| pub agent_token: String, | ||
| pub deployment_hash: String, | ||
| pub dashboard_url: Option<String>, | ||
| } | ||
|
|
||
| /// Collect server fingerprint from the local system. | ||
| pub fn collect_fingerprint() -> ServerFingerprint { | ||
| let hostname = std::env::var("HOSTNAME") | ||
| .or_else(|_| std::fs::read_to_string("/etc/hostname").map(|s| s.trim().to_string())) | ||
| .unwrap_or_else(|_| "unknown".to_string()); | ||
|
|
||
| let os = std::process::Command::new("uname") | ||
| .arg("-sr") | ||
| .output() | ||
| .ok() | ||
| .and_then(|o| String::from_utf8(o.stdout).ok()) | ||
| .map(|s| s.trim().to_string()) | ||
| .unwrap_or_else(|| "unknown".to_string()); | ||
|
|
||
| let cpu_count = std::fs::read_to_string("/proc/cpuinfo") | ||
| .map(|content| { | ||
| content | ||
| .lines() | ||
| .filter(|line| line.starts_with("processor")) | ||
| .count() as u32 | ||
| }) | ||
| .unwrap_or(1); | ||
|
|
||
| let ram_mb = std::fs::read_to_string("/proc/meminfo") | ||
| .ok() | ||
| .and_then(|content| { | ||
| content.lines().find_map(|line| { |
Comment on lines
+90
to
+117
| // Format: "MemTotal: 16384000 kB" | ||
| line.split_whitespace() | ||
| .nth(1) | ||
| .and_then(|v| v.parse::<u64>().ok()) | ||
| .map(|kb| kb / 1024) | ||
| } else { | ||
| None | ||
| } | ||
| }) | ||
| }) | ||
| .unwrap_or(0); | ||
|
|
||
| let disk_gb = std::process::Command::new("df") | ||
| .args(["--output=size", "-BG", "/"]) | ||
| .output() | ||
| .ok() | ||
| .and_then(|o| String::from_utf8(o.stdout).ok()) | ||
| .and_then(|s| { | ||
| s.lines() | ||
| .nth(1) // skip header | ||
| .and_then(|line| line.trim().trim_end_matches('G').parse::<u64>().ok()) | ||
| }) | ||
| .unwrap_or(0); | ||
|
|
||
| ServerFingerprint { | ||
| hostname, | ||
| os, | ||
| cpu_count, |
Comment on lines
+133
to
+148
| cpu_count = %fingerprint.cpu_count, | ||
| ram_mb = %fingerprint.ram_mb, | ||
| disk_gb = %fingerprint.disk_gb, | ||
| "collected server fingerprint" | ||
| ); | ||
|
|
||
| let body = RegistrationRequest { | ||
| purchase_token: purchase_token.to_string(), | ||
| server_fingerprint: fingerprint, | ||
| stack_id: stack_id.to_string(), | ||
| }; | ||
|
|
||
| let url = format!("{}/api/v1/agents/register", dashboard_url); | ||
| info!(url = %url, stack_id = %stack_id, "sending registration request to Stacker"); | ||
|
|
||
| let client = reqwest::Client::new(); |
Comment on lines
+156
to
+161
|
|
||
| let reg: RegistrationResponse = resp.json().await?; | ||
| info!( | ||
| agent_id = %reg.agent_id, | ||
| deployment_hash = %reg.deployment_hash, | ||
| "registration successful" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Registration/Auth from Status Panel web UI