Compare commits
14 changed files with 21 additions and 498 deletions
|
|
@ -113,8 +113,6 @@ read them à la carte.
|
||||||
- **"How does the matrix-tuwunel container work? What about
|
- **"How does the matrix-tuwunel container work? What about
|
||||||
fluffychat-web and per-agent matrix accounts?"** →
|
fluffychat-web and per-agent matrix accounts?"** →
|
||||||
[`docs/matrix.md`](docs/matrix.md).
|
[`docs/matrix.md`](docs/matrix.md).
|
||||||
- **"How do I give an agent a GitHub account (`gh` + `git push`)?
|
|
||||||
How is the PAT injected?"** → [`docs/github.md`](docs/github.md).
|
|
||||||
- **"How does DNS resolution work in agent containers? What's the
|
- **"How does DNS resolution work in agent containers? What's the
|
||||||
bridge network for?"** → [`docs/network.md`](docs/network.md).
|
bridge network for?"** → [`docs/network.md`](docs/network.md).
|
||||||
- **"How do I connect two hives into a swarm? How do I declare peer
|
- **"How do I connect two hives into a swarm? How do I declare peer
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -118,18 +118,6 @@ The hive-internal account is always named `main` (synthesized from `hyperhive.ma
|
||||||
|
|
||||||
For more details see [`docs/matrix.md`](docs/matrix.md).
|
For more details see [`docs/matrix.md`](docs/matrix.md).
|
||||||
|
|
||||||
### GitHub account
|
|
||||||
|
|
||||||
Every agent gets a managed GitHub identity — a `gh` CLI wrapper and `git push` over HTTPS — on by default (`hyperhive.github.enable`), inert until a PAT is provisioned. There is nothing per-agent to declare: paste an operator-supplied personal access token into the agent's dashboard **credentials** tab (or `hivectl github set-token <agent> --token-stdin`) and it works. The `gh` wrapper + git credential helper read the token live (git auths as `x-access-token` + the PAT; github.com only), so a rotated PAT takes effect with no rebuild.
|
|
||||||
|
|
||||||
Turn the integration off for the whole hive with the host option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.hyperhive.github.enable = false;
|
|
||||||
```
|
|
||||||
|
|
||||||
The PAT value is never in nix — only the enable flag. For more details see [`docs/github.md`](docs/github.md).
|
|
||||||
|
|
||||||
## Operator CLI
|
## Operator CLI
|
||||||
|
|
||||||
`hivectl` is the operator-facing host CLI for ad-hoc administration that
|
`hivectl` is the operator-facing host CLI for ad-hoc administration that
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
# GitHub accounts
|
|
||||||
|
|
||||||
Give an agent a managed GitHub identity — a `gh` CLI and `git push` over
|
|
||||||
HTTPS, both authenticated by an operator-supplied personal access token
|
|
||||||
(PAT) — so it can run GitHub API calls and push commits without any manual
|
|
||||||
`gh auth login`.
|
|
||||||
|
|
||||||
Provisioning is UI-driven, mirroring the dashboard side of the
|
|
||||||
[matrix account](matrix.md) flow: paste a PAT into the agent's credentials
|
|
||||||
tab and it works. No per-agent nix declaration, no rebuild — the token is
|
|
||||||
injected into the agent's state dir out of band.
|
|
||||||
|
|
||||||
## Enabling
|
|
||||||
|
|
||||||
The integration is **on by default** for every agent (`hyperhive.github.enable
|
|
||||||
= true`), inert until a PAT is provisioned. There is nothing per-agent to
|
|
||||||
declare — an agent gains GitHub simply by having a PAT written to its token
|
|
||||||
file.
|
|
||||||
|
|
||||||
To turn it off for the whole hive, set the host option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.hyperhive.github.enable = false;
|
|
||||||
```
|
|
||||||
|
|
||||||
hive-c0re's meta-flake renderer then injects `hyperhive.github.enable = false`
|
|
||||||
into every agent, so no agent ships the `gh` wrapper or credential helper.
|
|
||||||
(`hyperhive.github.enable` also exists per-agent for completeness, but the
|
|
||||||
hive-wide host switch is the intended control.)
|
|
||||||
|
|
||||||
github.com only. The token **value** never touches nix — it is written to
|
|
||||||
`<state>/github-token` separately (see [Provisioning](#provisioning)).
|
|
||||||
|
|
||||||
## How the agent uses it
|
|
||||||
|
|
||||||
When enabled, the container gets:
|
|
||||||
|
|
||||||
- **A `gh` wrapper** on `PATH` (shadowing the raw `gh`) that exports
|
|
||||||
`GH_TOKEN` from the token file at invocation, then execs real `gh`. So
|
|
||||||
`gh pr create`, `gh api …`, etc. just work — `gh` derives the identity
|
|
||||||
from the token.
|
|
||||||
- **A git credential helper** (`git-credential-hive-github`), wired via a
|
|
||||||
host-scoped `/etc/gitconfig` entry for `https://github.com`, so
|
|
||||||
`git push https://github.com/<owner>/<repo>` authenticates as
|
|
||||||
`x-access-token` + the PAT (GitHub ignores the username for PAT auth).
|
|
||||||
Host-scoped, so it never touches the forge (`localhost:3000`) or any
|
|
||||||
other remote.
|
|
||||||
- **Env**: `HIVE_GITHUB_TOKEN_FILE` (the token path — never the secret).
|
|
||||||
|
|
||||||
Both the wrapper and the credential helper read the token from the file
|
|
||||||
**at invocation time**, so a PAT written (or rotated) mid-session takes
|
|
||||||
effect immediately — no container rebuild or restart. Until the file
|
|
||||||
exists, `gh` / `git push` simply fail unauthenticated.
|
|
||||||
|
|
||||||
## Provisioning
|
|
||||||
|
|
||||||
The PAT is operator-supplied. The primary path is the **dashboard
|
|
||||||
credentials tab** (github sub-tab): paste the PAT for an agent and submit
|
|
||||||
(`POST /api/github-account`). There is also a CLI path for
|
|
||||||
recovery/scripting:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
hivectl github set-token <agent> --token-stdin # paste the PAT on stdin (preferred)
|
|
||||||
hivectl github set-token <agent> --token <pat> # inline (visible in shell history)
|
|
||||||
```
|
|
||||||
|
|
||||||
Either path has hive-c0re delegate the write to hive-priv, which stores the
|
|
||||||
file `0600` owned by the agent (so the container can read it) — the same
|
|
||||||
credential-injection path as forge/matrix tokens. See
|
|
||||||
[hivectl → GitHub](tools/hivectl.md#github).
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- Use a **dedicated bot account**, never a human's.
|
|
||||||
- Mint a **minimally-scoped PAT** — only the repos/scopes the agent's
|
|
||||||
workflow needs. Agents have passwordless sudo, so a compromised or
|
|
||||||
hallucinating agent can act as the account within the token's scopes;
|
|
||||||
scope is the real blast-radius limiter, and the container boundary is
|
|
||||||
the enforcement. See [security.md](security.md).
|
|
||||||
|
|
@ -13,8 +13,6 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
* [`hivectl matrix promote-user`↴](#hivectl-matrix-promote-user)
|
* [`hivectl matrix promote-user`↴](#hivectl-matrix-promote-user)
|
||||||
* [`hivectl matrix reset-password`↴](#hivectl-matrix-reset-password)
|
* [`hivectl matrix reset-password`↴](#hivectl-matrix-reset-password)
|
||||||
* [`hivectl matrix invite`↴](#hivectl-matrix-invite)
|
* [`hivectl matrix invite`↴](#hivectl-matrix-invite)
|
||||||
* [`hivectl github`↴](#hivectl-github)
|
|
||||||
* [`hivectl github set-token`↴](#hivectl-github-set-token)
|
|
||||||
* [`hivectl gateway`↴](#hivectl-gateway)
|
* [`hivectl gateway`↴](#hivectl-gateway)
|
||||||
* [`hivectl gateway create-user`↴](#hivectl-gateway-create-user)
|
* [`hivectl gateway create-user`↴](#hivectl-gateway-create-user)
|
||||||
* [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user)
|
* [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user)
|
||||||
|
|
@ -51,7 +49,6 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
||||||
|
|
||||||
* `forge` — Forgejo user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`forge::ensure_all`) — useful for recovery, ad-hoc reprovisioning, or single-agent fixes without bouncing the daemon
|
* `forge` — Forgejo user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`forge::ensure_all`) — useful for recovery, ad-hoc reprovisioning, or single-agent fixes without bouncing the daemon
|
||||||
* `matrix` — matrix-tuwunel user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`matrix::ensure_all`) — useful when the boot-time sweep skipped an agent (e.g. matrix container wasn't up yet) or to re-register after wiping a token file
|
* `matrix` — matrix-tuwunel user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`matrix::ensure_all`) — useful when the boot-time sweep skipped an agent (e.g. matrix container wasn't up yet) or to re-register after wiping a token file
|
||||||
* `github` — GitHub account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state file so its `gh` wrapper + git credential helper can authenticate. Unlike forge/matrix there is no account creation — the operator supplies a PAT for an existing GitHub account. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed
|
|
||||||
* `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
* `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
||||||
* `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
* `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
||||||
* `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`)
|
* `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`)
|
||||||
|
|
@ -197,35 +194,6 @@ Invite a matrix user to the hive Space (default) or a specific room. Uses the hi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl github`
|
|
||||||
|
|
||||||
GitHub account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state file so its `gh` wrapper + git credential helper can authenticate. Unlike forge/matrix there is no account creation — the operator supplies a PAT for an existing GitHub account. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed
|
|
||||||
|
|
||||||
**Usage:** `hivectl github <COMMAND>`
|
|
||||||
|
|
||||||
###### **Subcommands:**
|
|
||||||
|
|
||||||
* `set-token` — Write a GitHub PAT into `<agent>`'s state dir (`github-token`, 0600, agent-owned) via hive-priv. The GitHub integration is on by default (`hyperhive.github.enable`), so the `gh` wrapper + git credential helper pick the token up with no per-agent config. The token is read live at invocation, so no rebuild/restart is needed. Prefer `--token-stdin`: an inline `--token` is visible in shell history + process listings
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl github set-token`
|
|
||||||
|
|
||||||
Write a GitHub PAT into `<agent>`'s state dir (`github-token`, 0600, agent-owned) via hive-priv. The GitHub integration is on by default (`hyperhive.github.enable`), so the `gh` wrapper + git credential helper pick the token up with no per-agent config. The token is read live at invocation, so no rebuild/restart is needed. Prefer `--token-stdin`: an inline `--token` is visible in shell history + process listings
|
|
||||||
|
|
||||||
**Usage:** `hivectl github set-token [OPTIONS] <AGENT>`
|
|
||||||
|
|
||||||
###### **Arguments:**
|
|
||||||
|
|
||||||
* `<AGENT>` — Logical agent name (the container/agent name)
|
|
||||||
|
|
||||||
###### **Options:**
|
|
||||||
|
|
||||||
* `--token <TOKEN>` — The PAT value inline. Mutually exclusive with `--token-stdin`
|
|
||||||
* `--token-stdin` — Read the PAT from stdin (trailing newline stripped). Mutually exclusive with `--token`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl gateway`
|
## `hivectl gateway`
|
||||||
|
|
||||||
Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
||||||
|
|
|
||||||
|
|
@ -75,26 +75,6 @@ hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a spec
|
||||||
invite power (it owns the hive Space, so that case always works).
|
invite power (it owns the hive Space, so that case always works).
|
||||||
Idempotent — already-member / already-invited is a no-op.
|
Idempotent — already-member / already-invited is a no-op.
|
||||||
|
|
||||||
## GitHub
|
|
||||||
|
|
||||||
Write an operator-supplied GitHub personal access token (PAT) into an
|
|
||||||
agent's token file so its `gh` wrapper + git credential helper can act as
|
|
||||||
the bot account. Unlike forge/matrix there is no account creation — the PAT
|
|
||||||
is for an existing GitHub account. A CLI alternative to the dashboard
|
|
||||||
credentials tab; the [GitHub integration](../github.md) is on by default
|
|
||||||
(`hyperhive.github.enable`), so no per-agent config is needed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred)
|
|
||||||
hivectl github set-token damocles --token <pat> # inline (visible in shell history)
|
|
||||||
```
|
|
||||||
|
|
||||||
- `set-token`: writes `<state>/github-token` (`0600`, agent-owned) via
|
|
||||||
hive-priv — the same credential-injection path as forge/matrix tokens.
|
|
||||||
The `gh` wrapper / git credential helper read it live, so a freshly-set
|
|
||||||
or rotated PAT takes effect with no rebuild or restart. Refuses an empty
|
|
||||||
token. See [github.md](../github.md) for the full flow + security notes.
|
|
||||||
|
|
||||||
## Gateway
|
## Gateway
|
||||||
|
|
||||||
Manage users in the gateway's HTTP Basic auth htpasswd file
|
Manage users in the gateway's HTTP Basic auth htpasswd file
|
||||||
|
|
|
||||||
|
|
@ -70,17 +70,6 @@ enum Cmd {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: MatrixCmd,
|
cmd: MatrixCmd,
|
||||||
},
|
},
|
||||||
/// GitHub account provisioning: write an operator-supplied personal
|
|
||||||
/// access token (PAT) into an agent's `github-token` state file so its
|
|
||||||
/// `gh` wrapper + git credential helper can authenticate. Unlike
|
|
||||||
/// forge/matrix there is no account creation — the operator supplies a
|
|
||||||
/// PAT for an existing GitHub account. A CLI alternative to the dashboard
|
|
||||||
/// credentials tab; the integration is on by default
|
|
||||||
/// (`hyperhive.github.enable`), so no per-agent config is needed.
|
|
||||||
Github {
|
|
||||||
#[command(subcommand)]
|
|
||||||
cmd: GithubCmd,
|
|
||||||
},
|
|
||||||
/// Gateway htpasswd user management. Add, remove, or list users in
|
/// Gateway htpasswd user management. Add, remove, or list users in
|
||||||
/// an htpasswd file used by the gateway's HTTP Basic auth
|
/// an htpasswd file used by the gateway's HTTP Basic auth
|
||||||
/// (`services.hyperhive.gateway.auth`). Credentials are stored as
|
/// (`services.hyperhive.gateway.auth`). Credentials are stored as
|
||||||
|
|
@ -432,28 +421,6 @@ enum MatrixCmd {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum GithubCmd {
|
|
||||||
/// Write a GitHub PAT into `<agent>`'s state dir (`github-token`, 0600,
|
|
||||||
/// agent-owned) via hive-priv. The GitHub integration is on by default
|
|
||||||
/// (`hyperhive.github.enable`), so the `gh` wrapper + git credential
|
|
||||||
/// helper pick the token up with no per-agent config. The token is read
|
|
||||||
/// live at invocation, so no rebuild/restart is needed. Prefer
|
|
||||||
/// `--token-stdin`: an inline `--token` is visible in shell history +
|
|
||||||
/// process listings.
|
|
||||||
SetToken {
|
|
||||||
/// Logical agent name (the container/agent name).
|
|
||||||
agent: String,
|
|
||||||
/// The PAT value inline. Mutually exclusive with `--token-stdin`.
|
|
||||||
#[arg(long)]
|
|
||||||
token: Option<String>,
|
|
||||||
/// Read the PAT from stdin (trailing newline stripped). Mutually
|
|
||||||
/// exclusive with `--token`.
|
|
||||||
#[arg(long, conflicts_with = "token")]
|
|
||||||
token_stdin: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default htpasswd file path — the host-side location of the gateway's
|
// Default htpasswd file path — the host-side location of the gateway's
|
||||||
// credential store, pre-created by a tmpfiles rule when
|
// credential store, pre-created by a tmpfiles rule when
|
||||||
// `services.hyperhive.gateway.auth.enable = true`. Literal lives in
|
// `services.hyperhive.gateway.auth.enable = true`. Literal lives in
|
||||||
|
|
@ -651,13 +618,6 @@ async fn main() -> Result<()> {
|
||||||
MatrixCmd::ResetPassword { name } => matrix_reset_password(&name).await,
|
MatrixCmd::ResetPassword { name } => matrix_reset_password(&name).await,
|
||||||
MatrixCmd::Invite { user, room } => matrix_invite(&user, room.as_deref()).await,
|
MatrixCmd::Invite { user, room } => matrix_invite(&user, room.as_deref()).await,
|
||||||
},
|
},
|
||||||
Cmd::Github { cmd } => match cmd {
|
|
||||||
GithubCmd::SetToken {
|
|
||||||
agent,
|
|
||||||
token,
|
|
||||||
token_stdin,
|
|
||||||
} => github_set_token(&agent, token, token_stdin).await,
|
|
||||||
},
|
|
||||||
Cmd::Gateway { cmd } => match cmd {
|
Cmd::Gateway { cmd } => match cmd {
|
||||||
GatewayCmd::CreateUser {
|
GatewayCmd::CreateUser {
|
||||||
file,
|
file,
|
||||||
|
|
@ -1164,34 +1124,6 @@ fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
|
||||||
Err(anyhow::anyhow!("exec machinectl: {err}"))
|
Err(anyhow::anyhow!("exec machinectl: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `hivectl github set-token <agent>`: write an operator-supplied GitHub PAT
|
|
||||||
/// into the agent's `github-token` state file (0600, agent-owned) via
|
|
||||||
/// hive-priv, so the agent's `gh` wrapper + git credential helper can
|
|
||||||
/// authenticate. Read live at invocation, so no rebuild/restart is needed.
|
|
||||||
async fn github_set_token(agent: &str, token: Option<String>, token_stdin: bool) -> Result<()> {
|
|
||||||
let token = match (token, token_stdin) {
|
|
||||||
(Some(t), _) => t,
|
|
||||||
(None, true) => {
|
|
||||||
let mut s = String::new();
|
|
||||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
|
|
||||||
s.trim_end_matches(['\n', '\r']).to_owned()
|
|
||||||
}
|
|
||||||
(None, false) => bail!(
|
|
||||||
"provide the PAT via --token <pat> or --token-stdin (stdin preferred — \
|
|
||||||
an inline token is visible in shell history + process listings)"
|
|
||||||
),
|
|
||||||
};
|
|
||||||
if token.is_empty() {
|
|
||||||
bail!("refusing to write an empty GitHub token for agent '{agent}'");
|
|
||||||
}
|
|
||||||
hive_c0re::priv_client::write_agent_github_token(agent, &token).await?;
|
|
||||||
println!(
|
|
||||||
"wrote github-token for agent '{agent}' \
|
|
||||||
(read live by the gh wrapper / git credential helper — no rebuild needed)"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn forge_create_user(name: &str, password: Option<&str>, password_stdin: bool) -> Result<()> {
|
async fn forge_create_user(name: &str, password: Option<&str>, password_stdin: bool) -> Result<()> {
|
||||||
if !hive_c0re::forge::is_present().await {
|
if !hive_c0re::forge::is_present().await {
|
||||||
bail!(
|
bail!(
|
||||||
|
|
|
||||||
|
|
@ -262,73 +262,6 @@ pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) ->
|
||||||
axum::Json(MatrixLoginResult { ok: true, user_id }).into_response()
|
axum::Json(MatrixLoginResult { ok: true, user_id }).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Form body for `POST /api/github-account` (urlencoded, the dashboard's
|
|
||||||
/// mutation convention). Writes the operator-supplied PAT to the agent's
|
|
||||||
/// `github-token` file. The GitHub counterpart of the matrix login form, but
|
|
||||||
/// far simpler: no account creation, no homeserver, no login modes — the
|
|
||||||
/// operator pastes a PAT for an existing account.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub(super) struct GithubAccountForm {
|
|
||||||
agent: String,
|
|
||||||
token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct GithubAccountResult {
|
|
||||||
ok: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provision (or refresh) an agent's GitHub PAT from the dashboard
|
|
||||||
/// credentials tab. Validates the agent name, then writes the PAT to
|
|
||||||
/// `<state>/github-token` (`0600`, agent-owned) via hive-priv. No account
|
|
||||||
/// creation and no daemon to kick — the agent's `gh` wrapper / git credential
|
|
||||||
/// helper read the file live, so the new token takes effect immediately.
|
|
||||||
/// Operator-authenticated (dashboard). Never echoes the token back — only
|
|
||||||
/// `{ ok: true }`.
|
|
||||||
pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Response {
|
|
||||||
let agent = f.agent.trim();
|
|
||||||
let token = f.token.trim();
|
|
||||||
if !is_plain_ident(agent) {
|
|
||||||
return error_response(&format!("github-account: invalid agent {agent:?}"));
|
|
||||||
}
|
|
||||||
if token.is_empty() {
|
|
||||||
return error_response("github-account: token is required");
|
|
||||||
}
|
|
||||||
if let Err(e) = crate::priv_client::write_agent_github_token(agent, token).await {
|
|
||||||
return error_response(&format!("github-account: write token failed: {e:#}"));
|
|
||||||
}
|
|
||||||
tracing::info!(%agent, "github-account: provisioned github PAT");
|
|
||||||
axum::Json(GithubAccountResult { ok: true }).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub(super) struct GithubAccountQuery {
|
|
||||||
agent: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct GithubAccountStatus {
|
|
||||||
/// A `github-token` file exists in the agent's state dir (a PAT has been
|
|
||||||
/// provisioned). A static PAT has no live/heartbeat concept, so this is
|
|
||||||
/// the only status the credentials tab needs.
|
|
||||||
present: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `GET /api/github-account?agent=<name>` — whether the agent has a GitHub
|
|
||||||
/// PAT provisioned (its `github-token` file exists). Lets the credentials tab
|
|
||||||
/// show "token stored" vs "not set" instead of a black-hole paste field.
|
|
||||||
/// Never returns the token itself.
|
|
||||||
pub(super) async fn get_github_account(Query(q): Query<GithubAccountQuery>) -> Response {
|
|
||||||
let agent = q.agent.trim();
|
|
||||||
if !is_plain_ident(agent) {
|
|
||||||
return error_response(&format!("github-account: invalid agent {agent:?}"));
|
|
||||||
}
|
|
||||||
let present = Coordinator::agent_notes_dir(agent)
|
|
||||||
.join("github-token")
|
|
||||||
.exists();
|
|
||||||
axum::Json(GithubAccountStatus { present }).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// POST `m.login.password` to `<homeserver>/_matrix/client/v3/login`.
|
/// POST `m.login.password` to `<homeserver>/_matrix/client/v3/login`.
|
||||||
/// Returns `(access_token, user_id)`.
|
/// Returns `(access_token, user_id)`.
|
||||||
async fn matrix_password_login(
|
async fn matrix_password_login(
|
||||||
|
|
|
||||||
|
|
@ -188,10 +188,6 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
"/api/matrix-account-login",
|
"/api/matrix-account-login",
|
||||||
post(matrix_accounts::post_matrix_account_login),
|
post(matrix_accounts::post_matrix_account_login),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/github-account",
|
|
||||||
post(matrix_accounts::post_github_account).get(matrix_accounts::get_github_account),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/cancel-reminder/{id}",
|
"/api/cancel-reminder/{id}",
|
||||||
post(reminders::post_cancel_reminder),
|
post(reminders::post_cancel_reminder),
|
||||||
|
|
|
||||||
|
|
@ -1041,14 +1041,6 @@ where
|
||||||
out.push_str(" hyperhive.otel.debug = true;\n");
|
out.push_str(" hyperhive.otel.debug = true;\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GitHub integration is on by default in every agent
|
|
||||||
// (`hyperhive.github.enable`); the host turns it off hive-wide via
|
|
||||||
// `services.hyperhive.github.enable = false`, surfaced here as the
|
|
||||||
// `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF
|
|
||||||
// override is propagated — the enabled default needs no per-agent line.
|
|
||||||
if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() {
|
|
||||||
out.push_str(" hyperhive.github.enable = false;\n");
|
|
||||||
}
|
|
||||||
out.push_str(
|
out.push_str(
|
||||||
r#" # The harness service inside the container runs as a
|
r#" # The harness service inside the container runs as a
|
||||||
# non-root unix user named after the agent (`damocles`,
|
# non-root unix user named after the agent (`damocles`,
|
||||||
|
|
@ -1753,44 +1745,4 @@ mod tests {
|
||||||
"no otel lines when disabled:\n{off}"
|
"no otel lines when disabled:\n{off}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_flake_injects_github_disable_only_when_signalled() {
|
|
||||||
// services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED
|
|
||||||
// on hive-c0re's unit -> `hyperhive.github.enable = false` injected into
|
|
||||||
// every agent. On by default, so nothing is emitted unless disabled.
|
|
||||||
//
|
|
||||||
// SAFETY: single-threaded mutation of an env var no other test asserts
|
|
||||||
// on; restored before returning.
|
|
||||||
let render = || {
|
|
||||||
render_flake(
|
|
||||||
"github:example/hyperhive",
|
|
||||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
|
||||||
"path:/nix/store/aaaa-nixpkgs-source",
|
|
||||||
8000,
|
|
||||||
"she/her",
|
|
||||||
&std::collections::HashMap::new(),
|
|
||||||
&[sample_spec("alice", false, 9001)],
|
|
||||||
)
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
|
|
||||||
}
|
|
||||||
let on_default = render();
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var("HYPERHIVE_GITHUB_DISABLED", "1");
|
|
||||||
}
|
|
||||||
let disabled = render();
|
|
||||||
unsafe {
|
|
||||||
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
!on_default.contains("hyperhive.github.enable"),
|
|
||||||
"github.enable must not be emitted by default (agents keep the true default):\n{on_default}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
disabled.contains("hyperhive.github.enable = false;"),
|
|
||||||
"github.enable = false must be injected when the host disables it:\n{disabled}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -281,25 +281,6 @@ pub async fn write_agent_matrix_token(
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a GitHub personal access token (PAT) for `agent_name` via hive-priv
|
|
||||||
/// (running as root). Writes `<state>/github-token` 0600, chowned to the agent
|
|
||||||
/// user so the `gh` wrapper / git credential helper can read it from inside the
|
|
||||||
/// container. Single account per agent — no account suffix. The token value is
|
|
||||||
/// operator-supplied (for the agent's GitHub integration, `hyperhive.github.enable`).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the hive-priv call fails — the socket is unreachable,
|
|
||||||
/// `agent_name` is rejected by the root-side validation, or the file
|
|
||||||
/// write/chown fails.
|
|
||||||
pub async fn write_agent_github_token(agent_name: &str, token: &str) -> Result<()> {
|
|
||||||
ok(call(&PrivRequest::WriteAgentGithubToken {
|
|
||||||
agent_name: agent_name.to_owned(),
|
|
||||||
token: token.to_owned(),
|
|
||||||
})
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restart `hive-matrix-daemon.service` inside an agent container via
|
/// Restart `hive-matrix-daemon.service` inside an agent container via
|
||||||
/// `systemctl --machine=h-<agent_name> restart hive-matrix-daemon.service`.
|
/// `systemctl --machine=h-<agent_name> restart hive-matrix-daemon.service`.
|
||||||
/// Non-fatal: callers should handle errors gracefully — if the container is
|
/// Non-fatal: callers should handle errors gracefully — if the container is
|
||||||
|
|
|
||||||
|
|
@ -289,14 +289,6 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivRequest::WriteAgentGithubToken {
|
|
||||||
ref agent_name,
|
|
||||||
ref token,
|
|
||||||
} => {
|
|
||||||
validate_agent_name(agent_name)?;
|
|
||||||
write_agent_state_file(agent_name, "github-token", &format!("{token}\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivRequest::RestartMatrixDaemon { ref agent_name } => {
|
PrivRequest::RestartMatrixDaemon { ref agent_name } => {
|
||||||
restart_matrix_daemon(agent_name).await
|
restart_matrix_daemon(agent_name).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,22 +411,6 @@ pub enum PrivRequest {
|
||||||
homeserver: Option<String>,
|
homeserver: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Write `github-token` into `AGENT_STATE_ROOT/<agent_name>/state/github-token`.
|
|
||||||
///
|
|
||||||
/// The operator-supplied GitHub personal access token (PAT) for the
|
|
||||||
/// agent's GitHub integration (`hyperhive.github.enable`). Same write
|
|
||||||
/// semantics as
|
|
||||||
/// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir
|
|
||||||
/// if absent, writes the file 0600, and chowns it to the agent so the
|
|
||||||
/// `gh` wrapper / git credential helper can read it. No account suffix
|
|
||||||
/// (single GitHub account per agent).
|
|
||||||
WriteAgentGithubToken {
|
|
||||||
/// Logical agent name (validated by `validate_agent_name`).
|
|
||||||
agent_name: String,
|
|
||||||
/// PAT value. hive-priv appends a trailing newline before writing.
|
|
||||||
token: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Restart `hive-matrix-daemon.service` inside an agent container via
|
/// Restart `hive-matrix-daemon.service` inside an agent container via
|
||||||
/// `systemctl --machine=h-<agent_name> restart hive-matrix-daemon.service`.
|
/// `systemctl --machine=h-<agent_name> restart hive-matrix-daemon.service`.
|
||||||
/// Used by hive-c0re to kick the daemon after a successful token write
|
/// Used by hive-c0re to kick the daemon after a successful token write
|
||||||
|
|
|
||||||
|
|
@ -215,22 +215,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
options.services.hyperhive.github.enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
example = false;
|
|
||||||
description = ''
|
|
||||||
Hive-wide switch for the per-agent GitHub integration (the `gh` CLI
|
|
||||||
wrapper + git credential helper, per `hyperhive.github.enable`). On by
|
|
||||||
default: every agent gets the integration, inert until a PAT is
|
|
||||||
provisioned via the dashboard credentials tab or `hivectl github
|
|
||||||
set-token`. Set `false` to turn it off for the whole hive --- the
|
|
||||||
meta-flake renderer (`hive-c0re/src/meta.rs`) then injects
|
|
||||||
`hyperhive.github.enable = false` into every agent. Exposed to hive-c0re
|
|
||||||
as `HYPERHIVE_GITHUB_DISABLED` (set only when the integration is off).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Hive-wide OTEL stats export. Set ONCE here at host level; the
|
# Hive-wide OTEL stats export. Set ONCE here at host level; the
|
||||||
# meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the
|
# meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the
|
||||||
# HYPERHIVE_OTEL_* env exported below off hive-c0re's unit and injects
|
# HYPERHIVE_OTEL_* env exported below off hive-c0re's unit and injects
|
||||||
|
|
@ -972,12 +956,6 @@ in
|
||||||
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
|
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
|
||||||
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
|
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (!config.services.hyperhive.github.enable) {
|
|
||||||
# GitHub integration is on by default; only signal the OFF override to
|
|
||||||
# meta.rs, which then injects `hyperhive.github.enable = false` into
|
|
||||||
# every agent. See services.hyperhive.github.enable.
|
|
||||||
HYPERHIVE_GITHUB_DISABLED = "1";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs config.services.hyperhive.otel.enable (
|
// lib.optionalAttrs config.services.hyperhive.otel.enable (
|
||||||
# Hive-wide OTEL config -> read by meta.rs::otel_config and
|
# Hive-wide OTEL config -> read by meta.rs::otel_config and
|
||||||
# injected as build-time `hyperhive.otel.*` into every agent.
|
# injected as build-time `hyperhive.otel.*` into every agent.
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,6 @@ let
|
||||||
# from `userName` to keep them coupled.
|
# from `userName` to keep them coupled.
|
||||||
userName = config.hyperhive.user.name;
|
userName = config.hyperhive.user.name;
|
||||||
homeDir = "/home/${userName}";
|
homeDir = "/home/${userName}";
|
||||||
# GitHub integration (hyperhive.github.enable): a `gh` wrapper + a git
|
|
||||||
# credential helper, both sourcing the PAT from HIVE_GITHUB_TOKEN_FILE at
|
|
||||||
# invocation time so a dashboard-pasted token takes effect with no rebuild.
|
|
||||||
# The scripts are static (they read env at runtime) — the enable flag only
|
|
||||||
# gates whether they're installed + the env is set, so the token value never
|
|
||||||
# enters the nix store. github.com only; git auths as `x-access-token` + PAT.
|
|
||||||
ghWrapper = pkgs.writeShellScriptBin "gh" ''
|
|
||||||
if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then
|
|
||||||
GH_TOKEN="$(cat "''${HIVE_GITHUB_TOKEN_FILE}")"
|
|
||||||
export GH_TOKEN
|
|
||||||
fi
|
|
||||||
exec ${pkgs.gh}/bin/gh "$@"
|
|
||||||
'';
|
|
||||||
gitCredHelper = pkgs.writeShellScriptBin "git-credential-hive-github" ''
|
|
||||||
# git credential-helper protocol: only the `get` action needs an answer.
|
|
||||||
[ "''${1:-}" = "get" ] || exit 0
|
|
||||||
if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then
|
|
||||||
# GitHub ignores the username for PAT auth — `x-access-token` is the
|
|
||||||
# conventional placeholder; the PAT is the password.
|
|
||||||
printf 'username=x-access-token\n'
|
|
||||||
printf 'password=%s\n' "$(cat "''${HIVE_GITHUB_TOKEN_FILE}")"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
|
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
|
||||||
# meta.rs `otel_config`).
|
# meta.rs `otel_config`).
|
||||||
otelCfg = config.hyperhive.otel;
|
otelCfg = config.hyperhive.otel;
|
||||||
|
|
@ -647,31 +624,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
options.hyperhive.github.enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Install the GitHub integration in this agent: a `gh` CLI wrapper and a
|
|
||||||
git credential helper for `https://github.com`, both authenticated from
|
|
||||||
an operator-supplied personal access token (PAT). The PAT is written to
|
|
||||||
`<state>/github-token` out of band --- the dashboard credentials tab or
|
|
||||||
`hivectl github set-token` --- so giving an agent GitHub is a runtime
|
|
||||||
paste, no per-agent config or rebuild. The wrappers read the token file
|
|
||||||
at invocation, so a freshly-pasted PAT takes effect immediately; until
|
|
||||||
one exists, `gh` / `git push` just fail unauthenticated.
|
|
||||||
|
|
||||||
github.com only. git authenticates as `x-access-token` + the PAT (GitHub
|
|
||||||
ignores the username for PAT auth); `gh` derives its identity from the
|
|
||||||
token. Keep the PAT minimally scoped: the agent has passwordless sudo, so
|
|
||||||
a compromised agent can act within the token's scopes --- scope is the
|
|
||||||
real blast-radius limiter.
|
|
||||||
|
|
||||||
On by default. Host-driven: set `services.hyperhive.github.enable = false`
|
|
||||||
to turn the integration off hive-wide (meta.rs propagates the override
|
|
||||||
into every agent).
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
options.hyperhive.frontend.dist = lib.mkOption {
|
options.hyperhive.frontend.dist = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = pkgs.hyperhive-frontend;
|
default = pkgs.hyperhive-frontend;
|
||||||
|
|
@ -1536,12 +1488,6 @@ in
|
||||||
# regardless of which profile files are sourced.
|
# regardless of which profile files are sourced.
|
||||||
NIX_REMOTE = "daemon";
|
NIX_REMOTE = "daemon";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.hyperhive.github.enable {
|
|
||||||
# GitHub integration: point the gh wrapper + git credential helper at the
|
|
||||||
# agent's PAT file (written by the credentials tab / `hivectl github
|
|
||||||
# set-token`). Only the path — never the secret (see hyperhive.github.enable).
|
|
||||||
HIVE_GITHUB_TOKEN_FILE = "/agents/${userName}/state/github-token";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (!config.hyperhive.autoCompact) {
|
// lib.optionalAttrs (!config.hyperhive.autoCompact) {
|
||||||
# Zero watermark disables proactive compaction; the reactive path
|
# Zero watermark disables proactive compaction; the reactive path
|
||||||
# (compact-on-overflow) still fires when the session is truly full.
|
# (compact-on-overflow) still fires when the session is truly full.
|
||||||
|
|
@ -1636,53 +1582,27 @@ in
|
||||||
# we have to allow it inside the container's config as well.
|
# we have to allow it inside the container's config as well.
|
||||||
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
||||||
|
|
||||||
environment.systemPackages =
|
environment.systemPackages = with pkgs; [
|
||||||
with pkgs;
|
hyperhive
|
||||||
[
|
claude-code
|
||||||
hyperhive
|
bashInteractive
|
||||||
claude-code
|
coreutils-full
|
||||||
bashInteractive
|
# procps for pkill — used by the web UI's /api/cancel to SIGINT the
|
||||||
coreutils-full
|
# in-flight claude turn.
|
||||||
# procps for pkill — used by the web UI's /api/cancel to SIGINT the
|
procps
|
||||||
# in-flight claude turn.
|
# tea: gitea/forgejo CLI client. Configured at boot by the
|
||||||
procps
|
# tea-login oneshot below if /state/forge-token is present, so
|
||||||
# tea: gitea/forgejo CLI client. Configured at boot by the
|
# claude can `tea repos create`, `tea pulls create`, etc.
|
||||||
# tea-login oneshot below if /state/forge-token is present, so
|
tea
|
||||||
# claude can `tea repos create`, `tea pulls create`, etc.
|
# jq: JSON processing in shell — useful for parsing API responses,
|
||||||
tea
|
# forge REST calls, sqlite output, etc.
|
||||||
# jq: JSON processing in shell — useful for parsing API responses,
|
jq
|
||||||
# forge REST calls, sqlite output, etc.
|
# curl: HTTP client for forge REST API and other web requests.
|
||||||
jq
|
curl
|
||||||
# curl: HTTP client for forge REST API and other web requests.
|
# hive-forge <verb>: CLI wrapping common Forgejo REST API operations
|
||||||
curl
|
# (view, pr, issue, comment, assign, close, labels, branches, etc.)
|
||||||
# hive-forge <verb>: CLI wrapping common Forgejo REST API operations
|
(pkgs.callPackage ../packages/hive-forge-tools.nix { })
|
||||||
# (view, pr, issue, comment, assign, close, labels, branches, etc.)
|
];
|
||||||
(pkgs.callPackage ../packages/hive-forge-tools.nix { })
|
|
||||||
]
|
|
||||||
++ lib.optionals config.hyperhive.github.enable [
|
|
||||||
# gh wrapper + git credential helper for hyperhive.github.enable.
|
|
||||||
# (No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the real
|
|
||||||
# binary path, so it can't be shadowed.)
|
|
||||||
ghWrapper
|
|
||||||
gitCredHelper
|
|
||||||
];
|
|
||||||
|
|
||||||
# Wire the GitHub credential helper for `git push` over HTTPS. Host-scoped
|
|
||||||
# to `https://github.com`, so it never touches the forge (localhost:3000)
|
|
||||||
# or any other remote. Gated on hyperhive.github.enable; the helper reads
|
|
||||||
# the PAT from HIVE_GITHUB_TOKEN_FILE at invocation and auths as
|
|
||||||
# `x-access-token` + the PAT. System /etc/gitconfig merges under the agent's
|
|
||||||
# ~/.gitconfig (safe.directory), so this is additive.
|
|
||||||
# Nested-path binding + mkIf (matching the other `environment.etc."…"`
|
|
||||||
# entries above) — a whole-set `environment.etc = {…}` here would collide
|
|
||||||
# with them at the nix level ("attribute already defined").
|
|
||||||
environment.etc."gitconfig" = lib.mkIf config.hyperhive.github.enable {
|
|
||||||
text = ''
|
|
||||||
[credential "https://github.com"]
|
|
||||||
helper = hive-github
|
|
||||||
username = x-access-token
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# One-shot: tea config.yml from the seeded forge token. Shape
|
# One-shot: tea config.yml from the seeded forge token. Shape
|
||||||
# contract (always exit 0, no set -e, skip-silently, re-runnable):
|
# contract (always exit 0, no set -e, skip-silently, re-runnable):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue