fix(#2164): domain-URL webhooks + HMAC + config-PR polling fallback
Both webhook registrations (knowledge push + config-PR pull_request) now
use the public hive domain instead of loopback:
https://<HYPERHIVE_HIVE_DOMAIN>/webhook/{knowledge,config-pr}
This routes deliveries through the gateway, bypassing the Forgejo SSRF
guard that blocked loopback delivery and silently broke the config-PR
merge flow since launch.
Changes:
- webhook_secret: new module — auto-generate + persist a 32-byte HMAC
secret to STATE_ROOT/webhook-secret on first startup; verify
X-Hub-Signature-256 on every incoming webhook POST (HMAC-SHA256).
- forge/mod.rs: ensure_config_pr_webhook now takes hive_domain +
webhook_secret; sets secret in Forgejo hook config.
- workers/knowledge.rs: ensure_webhook same update.
- dashboard/webhook.rs: both handlers read raw Bytes first, verify HMAC,
then parse JSON. Returns 401 on signature mismatch.
- dashboard/mod.rs: AppState carries webhook_secret; serve() takes it.
- main.rs: load/generate secret at startup; pass to registration tasks
+ dashboard; add 5-minute config-PR polling fallback task.
- forge/config_pr_poll.rs: new — scan agent-configs/* for open PRs with
no pending MergeConfigPr approval; queue them. Idempotent.
- stores/approvals.rs: has_pending_merge_config_pr() for poll dedup.
- nix/modules/hive-gateway.nix: remove dashboardAuth from /webhook/
location (HMAC replaces basic auth for webhook endpoints; Forgejo
cannot send HTTP Basic credentials with webhook deliveries).
This commit is contained in:
parent
a99508719e
commit
79a29873e3
14 changed files with 434 additions and 37 deletions
|
|
@ -144,20 +144,30 @@ async fn seed_readme(core_token: &str) -> Result<()> {
|
|||
/// Ensure a Forgejo push webhook for `internal/knowledge` exists and
|
||||
/// points at hive-c0re's `/webhook/knowledge` endpoint. Idempotent —
|
||||
/// lists existing hooks first and skips creation when one is already
|
||||
/// targeting the correct URL. `dashboard_port` is the TCP port
|
||||
/// hive-c0re's dashboard listens on (default 7000); the webhook URL
|
||||
/// is `http://127.0.0.1:<port>/webhook/knowledge`.
|
||||
/// targeting the correct URL.
|
||||
///
|
||||
/// `hive_domain` is the public domain name of the hive; the webhook URL is
|
||||
/// `https://<hive_domain>/webhook/knowledge` (routed through the gateway,
|
||||
/// avoiding the Forgejo SSRF guard that blocks loopback delivery).
|
||||
///
|
||||
/// `webhook_secret` is the HMAC secret Forgejo will attach as
|
||||
/// `X-Hub-Signature-256` on each delivery; hive-c0re verifies this header
|
||||
/// in [`crate::dashboard::webhook::post_webhook_knowledge`].
|
||||
///
|
||||
/// Called at startup alongside [`ensure_local_clone`]. No-op when the
|
||||
/// core token is absent (forge not yet provisioned).
|
||||
pub async fn ensure_webhook(core_token: &str, dashboard_port: u16) -> Result<()> {
|
||||
pub async fn ensure_webhook(
|
||||
core_token: &str,
|
||||
hive_domain: &str,
|
||||
webhook_secret: &str,
|
||||
) -> Result<()> {
|
||||
// The typed client carries no per-request timeout, so each call is
|
||||
// wrapped in one: this runs as a detached startup task, and a forge
|
||||
// that accepts connections but never answers would otherwise hang
|
||||
// it forever (and the hourly pull fallback masks the missing hook).
|
||||
const HTTP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
let target_url = format!("http://127.0.0.1:{dashboard_port}/webhook/knowledge");
|
||||
let target_url = format!("https://{hive_domain}/webhook/knowledge");
|
||||
let client = crate::forge::api(core_token)?;
|
||||
|
||||
// List existing hooks — skip creation if ours is already there.
|
||||
|
|
@ -187,6 +197,9 @@ pub async fn ensure_webhook(core_token: &str, dashboard_port: u16) -> Result<()>
|
|||
}
|
||||
|
||||
// Create the webhook.
|
||||
let mut additional = BTreeMap::new();
|
||||
additional.insert("secret".to_owned(), webhook_secret.to_owned());
|
||||
|
||||
let hook = CreateHookOption {
|
||||
active: Some(true),
|
||||
authorization_header: None,
|
||||
|
|
@ -194,7 +207,7 @@ pub async fn ensure_webhook(core_token: &str, dashboard_port: u16) -> Result<()>
|
|||
config: CreateHookOptionConfig {
|
||||
content_type: "json".to_owned(),
|
||||
url: url::Url::parse(&target_url).context("parse webhook target url")?,
|
||||
additional: BTreeMap::new(),
|
||||
additional,
|
||||
},
|
||||
events: Some(vec!["push".to_owned()]),
|
||||
r#type: CreateHookOptionType::Forgejo,
|
||||
|
|
|
|||
Loading…
Reference in a new issue