extra-forges: fully dashboard-provisioned, no host config
Per mara's feedback on PR #2407 ("better: you can also provide url in dashboard, same as with matrix, no host config"), drops services.hyperhive.extraForges and the admin-API mint/revoke flow entirely. The operator now creates a token on the external forge themselves and pastes a label + base URL + access token into the dashboard's FORGES tab, the same shape as the GitHub PAT flow plus the base-URL field from the matrix extra-account flow. hive-c0re only ever writes/deletes two local files per account (forge-<label>-token, forge-<label>.json sidecar for the URL) via hive-priv — no remote account creation, no admin token, no revoke-on-the-remote-side, no nix config to enumerate. - nix/host-modules/hive-forge/default.nix: removed the extraForges option, its label-format assertion, and the HYPERHIVE_EXTRA_FORGES env forwarding. - hive-c0re/src/forge/extra.rs: deleted (REST admin-API provisioning, no longer needed). - hive-c0re/src/dashboard/extra_forges.rs: GET /api/extra-forges? agent= lists an agent's stored forges by scanning its state dir (mirrors matrix_accounts.rs's filename-scan listing), POST /api/extra-forge-account (agent/label/base_url/token/ action=add|remove) stores or removes an account. - hive-sh4re/priv_proto.rs + hive-priv/main.rs: new WriteAgentExtraForgeAccount/DeleteAgentExtraForgeAccount priv requests (adds base_url, writes/deletes a JSON sidecar alongside the token). - hive-c0re/src/priv_client.rs: matching wrapper functions. - frontend/packages/dashboard/src/credentials.{html,js}: FORGES tab is a per-agent list + add-account paste form (label/base_url/token), no grant/revoke-from-catalog UI. - docs/web-ui/dashboard.md: FORGES tab section rewritten. Supersedes the design in PR #2407 (already approved+green on the old admin-API model) — opening as a fresh PR against the same issues rather than force-pushing over the approved one.
This commit is contained in:
parent
f025f32ccb
commit
dbf880ac66
10 changed files with 558 additions and 2 deletions
|
|
@ -297,6 +297,36 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
write_agent_state_file(agent_name, "github-token", &format!("{token}\n"))
|
||||
}
|
||||
|
||||
PrivRequest::WriteAgentExtraForgeAccount {
|
||||
ref agent_name,
|
||||
ref label,
|
||||
ref base_url,
|
||||
ref token,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_name_chars(label)?;
|
||||
let res = write_agent_state_file(
|
||||
agent_name,
|
||||
&format!("forge-{label}-token"),
|
||||
&format!("{token}\n"),
|
||||
)?;
|
||||
// Sidecar carries the base URL — there's no host-side nix config
|
||||
// for extra forges, so this is the only place it's persisted.
|
||||
let meta = serde_json::json!({ "base_url": base_url }).to_string();
|
||||
write_agent_state_file(agent_name, &format!("forge-{label}.json"), &meta)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
PrivRequest::DeleteAgentExtraForgeAccount {
|
||||
ref agent_name,
|
||||
ref label,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_name_chars(label)?;
|
||||
delete_agent_state_file(agent_name, &format!("forge-{label}-token"))?;
|
||||
delete_agent_state_file(agent_name, &format!("forge-{label}.json"))
|
||||
}
|
||||
|
||||
PrivRequest::RestartMatrixDaemon { ref agent_name } => {
|
||||
restart_matrix_daemon(agent_name).await
|
||||
}
|
||||
|
|
@ -636,6 +666,30 @@ fn write_agent_state_file(
|
|||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// Remove `AGENT_STATE_ROOT/<agent_name>/state/<filename>` if present.
|
||||
/// Idempotent revoke counterpart to [`write_agent_state_file`] — a
|
||||
/// missing file is success, not an error. `filename` must be a single
|
||||
/// plain component (no `/`, `.`, `..`); callers pass a pre-validated
|
||||
/// label into a fixed `forge-<label>-token` shape, same as the write
|
||||
/// side.
|
||||
fn delete_agent_state_file(agent_name: &str, filename: &str) -> Result<(String, String)> {
|
||||
if filename.is_empty() || filename == "." || filename == ".." || filename.contains('/') {
|
||||
bail!("delete_agent_state_file: refusing non-plain filename {filename:?}");
|
||||
}
|
||||
let path = PathBuf::from(AGENT_STATE_ROOT)
|
||||
.join(agent_name)
|
||||
.join("state")
|
||||
.join(filename);
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
tracing::info!(agent = %agent_name, file = %filename, "removed agent state file");
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e).with_context(|| format!("remove {}", path.display())),
|
||||
}
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// btrfs superblock magic, as reported by `statfs(2)`'s `f_type`.
|
||||
const BTRFS_SUPER_MAGIC: i64 = 0x9123_683E;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue