github: PATs live in swarm bao; the agent fetches them itself
An operator links an agent's GitHub personal access token in the swarm UI
(LinkGithubAccountForm, "link github account" on /agents). swarm-controller's
PUT /api/hives/{hive}/agents/{agent}/github-account stores it at
swarm/agents/<agent>/github-token (swarm_secret_client::github), a flat leaf
under the agent's prefix that the agent's existing read grant already covers:
no policy change, and no list grant, since there is one token per agent.
In the agent, hive-agent-github-token (oneshot + 2-minute timer, as the agent
user, under its own store certificate, ordered before hive-github-notify)
reads that path and writes <state>/github-token, 0600 and agent-owned, the
file the gh wrapper, git credential helper and hive-github-notify already
read. It replaces the file by rename only when the bytes changed and never
deletes it: a hive-written github-token stays until a token is linked in the
swarm UI. It is installed only with a store address and
services.hyperhive.agent.github.enable.
Removed: the dashboard's CR3D3NTIALS page (credentials.html/js/css, its
build entries and H0M3 tile; GITHUB was its only tab), hive-c0re's
dashboard/matrix_accounts.rs with GET/POST /api/github-account,
priv_client::write_agent_github_token, the host socket's
SetAgentGithubToken and `hivectl github set-token`, and hive-priv's
WriteAgentGithubToken with write_agent_state_file, its only caller gone.
Docs: integrations/github.md and swarm/ui.md describe the swarm path,
swarm/credentials.md gains the store-path row, and the hive UI docs,
hivectl docs and security.md's hive-priv table drop the removed pieces.
Closes #4347
This commit is contained in:
parent
2b2608a491
commit
8e23feb01b
41 changed files with 804 additions and 846 deletions
|
|
@ -5,9 +5,9 @@ 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: paste a PAT into the agent's credentials tab
|
||||
and it works. No per-agent nix declaration, no rebuild — hive-c0re
|
||||
injects the token into the agent's state dir out of band.
|
||||
Provisioning is UI-driven: link a PAT to the agent in the swarm UI and it
|
||||
works. No per-agent nix declaration, no rebuild — the agent fetches the token
|
||||
from the swarm secret store itself.
|
||||
|
||||
## Enabling
|
||||
|
||||
|
|
@ -15,8 +15,7 @@ injects the token into the agent's state dir out of band.
|
|||
|
||||
The integration is **on by default** for every agent (`services.hyperhive.agent.github.enable
|
||||
= true`), inert until the operator provisions a PAT. No per-agent declaration is
|
||||
needed — an agent gains GitHub by having a PAT written to its token
|
||||
file.
|
||||
needed — an agent gains GitHub by having a PAT stored for it.
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
|
|
@ -27,29 +26,31 @@ services.hyperhive.github.enable = false;
|
|||
```
|
||||
|
||||
hive-c0re's meta-flake renderer then injects `services.hyperhive.agent.github.enable = false`
|
||||
into every agent, so no agent ships the `gh` wrapper or credential helper.
|
||||
into every agent, so no agent ships the `gh` wrapper, the credential helper
|
||||
or the token fetch.
|
||||
(`services.hyperhive.agent.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's written to
|
||||
`<state>/github-token` separately (see [Provisioning](#provisioning)).
|
||||
github.com only. The token **value** never touches nix — the agent writes it
|
||||
to `<state>/github-token` at runtime (see [Provisioning](#provisioning)).
|
||||
|
||||
## 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`). A CLI path also exists for
|
||||
recovery/scripting:
|
||||
The PAT is operator-supplied. In the [swarm UI](../swarm/ui.md#linking-external-accounts),
|
||||
open the agent on `/agents`, choose **link github account** and paste the PAT
|
||||
(`PUT /api/hives/{hive}/agents/{agent}/github-account`). swarm-controller
|
||||
stores it at `swarm/agents/<agent>/github-token` in the swarm secret store;
|
||||
no hive writes it. One token per agent: linking again replaces it,
|
||||
and no route hands it back.
|
||||
|
||||
```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).
|
||||
The agent's `hive-agent-github-token` unit reads that path under the
|
||||
agent's own store certificate and writes `<state>/github-token` (`0600`,
|
||||
owned by the agent), on boot and every two minutes, replacing the file only
|
||||
when the token changed. It needs a store identity
|
||||
(`services.hyperhive.agent.bao.addr`); an agent without one gets no token.
|
||||
The unit never deletes the file: a `github-token` already in place stays
|
||||
when the store holds none or doesn't answer. Where the token lives and who
|
||||
reads it: [credentials.md](../swarm/credentials.md).
|
||||
|
||||
## Security
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ of the cell says how.
|
|||
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | `hive-agent` in the agent container, under the agent's own certificate, held in memory — the identity it presents to the swarm queue, naming that one agent rather than its hive | ✅ `swarm-controller`'s five-minute pass re-mints a live agent's secret once it's 45 days old by `minted_at` on the stored object; a secret with no `minted_at` gets one stamped, value unchanged. The pass skips agents declared `Destroyed` — declaring an agent destroyed deletes every version of the path instead, the undo of the mint rather than another one | ✅ `hive-agent` reads the path before its first connect and again on every reconnect attempt, so a reconnect after a re-mint presents the new secret. An open connection keeps the secret it connected with; after a revocation the agent keeps retrying under the queue client's backoff |
|
||||
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | ✅ the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the agent re-fetches on a 10-minute timer |
|
||||
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when an operator links an external forge account in the swarm UI | `hive-agent-forge-accounts` in the agent container, under the agent's own certificate, into `<state>/forge-<label>-token` and `forge-<label>.json` | ❌ an operator's token; replaced only by linking the label again | ✅ the agent re-fetches on a 2-minute timer |
|
||||
| `swarm/agents/<agent>/github-token` | `swarm-controller`, when an operator links a GitHub account in the swarm UI | `hive-agent-github-token` in the agent container, under the agent's own certificate, into `<state>/github-token` | ❌ an operator's token; replaced only by linking it again | ✅ the agent re-fetches on a 2-minute timer |
|
||||
| `swarm/hives/<hive>/matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | must be stated |
|
||||
| `swarm/hives/<hive>/matrix/sender-token` | `swarm-controller`, with the swarm's appservice token, for every hive in its directory in a five-minute pass | `swarm-controller` under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | ✅ the controller's pass re-mints when the stored token is missing, unknown to the homeserver, or someone else's | ✅ hive-c0re's matrix sweep reads the store every run and overwrites its token file when the store's token differs |
|
||||
| `swarm/hives/<hive>/queue/agent` | authelia | `swarm-bao-queue-agent` on the hive's host, under its own per-hive certificate; no agent's policy reaches it | must be stated | must be stated |
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ domain, covers host-level detail for one hive.
|
|||
|
||||
## What it shows
|
||||
|
||||
| route | what |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `/` | the hive directory, each hive with its last reported status |
|
||||
| `/agents` | every agent: status, config PR, wanted state; create agents, link forge and matrix accounts |
|
||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||
| `/issues` | a cross-repo issue report |
|
||||
| route | what |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `/` | the hive directory, each hive with its last reported status |
|
||||
| `/agents` | every agent: status, config PR, wanted state; create agents, link forge, matrix and GitHub accounts |
|
||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||
| `/issues` | a cross-repo issue report |
|
||||
|
||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
||||
|
|
@ -21,8 +21,8 @@ An agent created here or with `swarmctl agent create` starts `paused`; set it
|
|||
|
||||
### Linking external accounts
|
||||
|
||||
Each agent on `/agents` opens two dialogs that write a credential for it
|
||||
into the swarm secret store through swarm-controller. Both are blind
|
||||
Each agent on `/agents` opens three dialogs that write a credential for it
|
||||
into the swarm secret store through swarm-controller. All three are blind
|
||||
set/update actions: no route lists linked accounts or hands a token back.
|
||||
|
||||
- **link a matrix account** — `PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}`,
|
||||
|
|
@ -36,6 +36,14 @@ set/update actions: no route lists linked accounts or hands a token back.
|
|||
`hive-forge -f <label>` reads. The unit never deletes a pair: linking
|
||||
the same label again overwrites both files, and a pair whose label the
|
||||
store doesn't list stays untouched.
|
||||
- **link a github account** — `PUT /api/hives/{hive}/agents/{agent}/github-account`
|
||||
with a personal access token, stored at `swarm/agents/<agent>/github-token`.
|
||||
One token per agent: linking again replaces it. The agent's
|
||||
`hive-agent-github-token` unit fetches it into `<state>/github-token`,
|
||||
the file its `gh` wrapper, git credential helper and GitHub notification
|
||||
poller read. A `github-token` already in place stays when the store holds
|
||||
none. What the token needs and how the agent uses it:
|
||||
[GitHub accounts](../integrations/github.md).
|
||||
|
||||
Where each credential lives and who reads it:
|
||||
[`credentials.md`](credentials.md).
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ This document contains the help content for the `hivectl` command-line program.
|
|||
* [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config)
|
||||
* [`hivectl matrix`↴](#hivectl-matrix)
|
||||
* [`hivectl matrix invite`↴](#hivectl-matrix-invite)
|
||||
* [`hivectl github`↴](#hivectl-github)
|
||||
* [`hivectl github set-token`↴](#hivectl-github-set-token)
|
||||
* [`hivectl gateway`↴](#hivectl-gateway)
|
||||
* [`hivectl gateway create-user`↴](#hivectl-gateway-create-user)
|
||||
* [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user)
|
||||
|
|
@ -64,7 +62,6 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
|||
|
||||
* `forge` — Reconcile an agent's config between this hive and the forge
|
||||
* `matrix` — matrix-tuwunel invites
|
||||
* `github` — GitHub account provisioning
|
||||
* `gateway` — Gateway htpasswd user management
|
||||
* `agent` — Lifecycle actions on ONE managed agent container. Needs the hive-c0re daemon running
|
||||
* `list-agents` — Show all managed agents with their status and technical state
|
||||
|
|
@ -156,39 +153,6 @@ Invite a matrix user to the hive Space, or a specific room with `--room`. Idempo
|
|||
|
||||
|
||||
|
||||
## `hivectl github`
|
||||
|
||||
GitHub account provisioning.
|
||||
|
||||
Store an operator-supplied personal access token (PAT) for an agent so its `gh` and git can authenticate. Creates no account — the PAT is for an existing GitHub account.
|
||||
|
||||
**Usage:** `hivectl github <COMMAND>`
|
||||
|
||||
###### **Subcommands:**
|
||||
|
||||
* `set-token` — Store a GitHub PAT for `<agent>` so its `gh` and git can authenticate
|
||||
|
||||
|
||||
|
||||
## `hivectl github set-token`
|
||||
|
||||
Store a GitHub PAT for `<agent>` so its `gh` and git can authenticate.
|
||||
|
||||
Prefer `--token-stdin` — an inline `--token` is visible in shell history.
|
||||
|
||||
**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`
|
||||
|
||||
Gateway htpasswd user management.
|
||||
|
|
|
|||
|
|
@ -65,30 +65,6 @@ group; nobody has built that sync yet.
|
|||
invite power (it owns the hive Space, so that case always works).
|
||||
Idempotent — already-member / already-invited is a no-op.
|
||||
|
||||
## GitHub
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
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 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](../integrations/github.md) is on by default
|
||||
(`services.hyperhive.agent.github.enable`), so no per-agent config is needed.
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
```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](../integrations/github.md) for the full flow + security notes.
|
||||
|
||||
## Gateway
|
||||
|
||||
Manage users in the gateway's HTTP Basic auth htpasswd file
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ known operations; there is no arbitrary command pass-through:
|
|||
| `ControlInfraContainer` | `systemctl <action> container@<container>.service` — the `InfraContainer` enum is the allowlist, and serde rejects unknown names at the wire boundary (`hive-c0re` has no variant, so no request can name it) |
|
||||
| `SyncAgentTmpfiles` | unlink `/etc/tmpfiles.d/hyperhive-agents.conf` and return `Ok` |
|
||||
| `SetAgentPaused` | create / remove the `<state>/<name>/harness/paused` marker that parks an agent's turn loop |
|
||||
| `WriteAgentGithubToken` | write `0600` `github-token` into agent state dir |
|
||||
| `RegisterCiRunner` | write `/run/hive-ci/runner-token` (host path, root-owned) then `systemctl --machine=hive-ci restart gitea-runner-hive.service`. Only the registration token crosses; the forge admin token never enters the container |
|
||||
| `EnsureAgentSubvolume` | `btrfs subvolume create <state>/<name>` for a new agent — no-op when the path exists or the filesystem isn't btrfs |
|
||||
| `UpgradeAgentSubvolume` | migrate an existing plain state dir into a subvolume: create, `cp -a --reflink=auto`, atomic swap. Operator opt-in, and the caller stops the agent first |
|
||||
|
|
|
|||
|
|
@ -29,11 +29,8 @@ Everything else lives on its own page instead, all reachable from the
|
|||
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
|
||||
the whole swarm), **Logs** (`/logs.html`, per-agent and host
|
||||
journals), **Stats** (`/stats.html`, swarm-wide usage stats),
|
||||
**Builds** (`/builds.html`, the rebuild queue and build history),
|
||||
**Core** (`/core.html`, tombstones and container resource use), and
|
||||
**Credentials** (`/credentials.html`, pasting a GitHub PAT for an
|
||||
agent's dedicated bot account — matrix and external-forge accounts are
|
||||
swarm identities, linked from the swarm UI). Your local
|
||||
**Builds** (`/builds.html`, the rebuild queue and build history), and
|
||||
**Core** (`/core.html`, tombstones and container resource use). Your local
|
||||
browser preferences (notifications) live in the dashboard's Y3R C4LL
|
||||
tab.
|
||||
|
||||
|
|
@ -83,11 +80,6 @@ journald view for any agent + service; the per-agent `⋮` menu's
|
|||
recurring or one-shot prompts you schedule for one or more agents, and
|
||||
reminders agents have set for themselves.
|
||||
|
||||
**Set an agent's GitHub token.** The Credentials page pastes a PAT for
|
||||
the agent's dedicated bot account, without editing that agent's config
|
||||
repo. Link matrix and external-forge accounts from the swarm UI
|
||||
instead.
|
||||
|
||||
## More depth
|
||||
|
||||
Both the dashboard and the per-agent pages are SPAs sharing one
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from the dashboard tab strip.
|
|||
— sits below the tab strip.
|
||||
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
|
||||
on **every** page (dashboard + every stand-alone page — FL0W, L0GS,
|
||||
H0M3, C0R3, BU1LDS, CR3D3NTIALS, ST4TS), injected at the top
|
||||
H0M3, C0R3, BU1LDS, ST4TS), injected at the top
|
||||
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
|
||||
`state.server_warnings` — a list of `{ kind, level, message }` — and
|
||||
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the
|
||||
|
|
@ -283,35 +283,6 @@ badge with a ticking elapsed-time chip; expanding streams output via
|
|||
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
|
||||
entry with that id pre-expanded.
|
||||
|
||||
## CR3D3NTIALS page (`/credentials.html`)
|
||||
|
||||
Operator surface to provision per-agent credentials without editing the
|
||||
agent's config repo. Standalone page reached from the **Credentials** tile
|
||||
on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link
|
||||
+ a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather
|
||||
than `/core.html`'s plain title. Its own esbuild bundle
|
||||
(`credentials.js`); no SSE — it reads `/api/state` once for the (shared)
|
||||
agent picker and otherwise works off purpose-built endpoints per tab.
|
||||
One sub-tab, GITHUB.
|
||||
|
||||
### GITHUB tab
|
||||
|
||||
Provision a single per-agent GitHub personal access token (see
|
||||
[`docs/integrations/github.md`](../integrations/github.md) for the injection + `gh`/git-push
|
||||
mechanics). No login flow — the operator pastes an existing PAT for a
|
||||
dedicated bot account, with a security-warning banner (dedicated account +
|
||||
minimally scoped token) and a link to
|
||||
[github.com/settings/tokens](https://github.com/settings/tokens).
|
||||
|
||||
Status reads `GET /api/github-account?agent=<name>` →
|
||||
`{ present: bool }` — whether the agent's `github-token` file exists.
|
||||
There's no live/heartbeat concept for a static PAT, so this is just a
|
||||
"token stored ✓" / "not set" line.
|
||||
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
|
||||
`token`) → `200 { ok: true }` on success; failures come back as RFC 9457
|
||||
`application/problem+json` with the message in `detail`. The token is never
|
||||
echoed back in either direction.
|
||||
|
||||
## P3RM1SS10NS tab
|
||||
|
||||
Per-agent permission configuration. Two sections, each rendered as a
|
||||
|
|
@ -575,7 +546,7 @@ re-renders the terminal row. The operator addresses the root agent as `@root`.
|
|||
|
||||
The H0M3 hub is the primary landing page (served at `/` by default). A
|
||||
responsive grid of link tiles — Dashboard, Flow, Logs, Builds, Stats,
|
||||
Core, Credentials, API — each pointing to their respective
|
||||
Core, API — each pointing to their respective
|
||||
surfaces, all unconditionally shown (no gating). The page is a pure
|
||||
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
|
||||
from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
|
||||
loaded by every page, plus a page-specific bundle —
|
||||
`dashboard.css` / `flow.css` / `logs.css` / `home.css` /
|
||||
`stats.css` / `core.css` / `builds.css` /
|
||||
`credentials.css`); `common.css` inlines `@hive/shared`'s
|
||||
`stats.css` / `core.css` / `builds.css`);
|
||||
`common.css` inlines `@hive/shared`'s
|
||||
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
|
||||
`pill.css` via esbuild's `@import` resolution.
|
||||
`terminal.js` exports `{ create, linkify }` as ES module
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at
|
||||
// `/`, dashboard/flow/logs/core/builds/credentials at their own
|
||||
// `/`, dashboard/flow/logs/core/builds at their own
|
||||
// `.html`), each named after its own entry point below — see the
|
||||
// `entryPoints`/`for` lists for the exact map, not repeated here to
|
||||
// avoid this comment drifting out of sync with the real build steps.
|
||||
|
|
@ -40,7 +40,6 @@ await build({
|
|||
src("stats.js"),
|
||||
src("core.js"),
|
||||
src("builds.js"),
|
||||
src("credentials.js"),
|
||||
],
|
||||
outdir: staticDir(""),
|
||||
bundle: true,
|
||||
|
|
@ -107,7 +106,6 @@ for (const entry of [
|
|||
"stats.css",
|
||||
"core.css",
|
||||
"builds.css",
|
||||
"credentials.css",
|
||||
]) {
|
||||
await build({
|
||||
entryPoints: [src(entry)],
|
||||
|
|
@ -126,7 +124,6 @@ for (const html of [
|
|||
"stats.html",
|
||||
"core.html",
|
||||
"builds.html",
|
||||
"credentials.html",
|
||||
]) {
|
||||
copyFileSync(src(html), dist(html));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
/* CR3D3NTIALS page (/credentials.html) only. Page chrome
|
||||
(.page-header / .page-back / .page-title) comes from the shared
|
||||
chrome.css imported by common.css; base tab styling lives in
|
||||
@hive/shared/tabs.css (.hive-tab*) same as /logs.html. This file holds
|
||||
the account-list + provision-form styling specific to this surface
|
||||
(`.ma-*` classes) plus the tab-strip layout delta + github-tab
|
||||
additions (`.cred-*`). */
|
||||
|
||||
body.cred-shell {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Same layout delta as .logs-tabbar: fill the header row next to the
|
||||
← home back-link, and let the flex:1 nav shrink below its intrinsic
|
||||
width instead of wrapping onto its own row. */
|
||||
.cred-tabbar {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cred-main {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1.2em 1.25rem 3rem;
|
||||
}
|
||||
|
||||
.cred-pane[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gh-status {
|
||||
margin: 0.5rem 0 1.2rem;
|
||||
}
|
||||
.gh-status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.gh-dot {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
.gh-dot.present {
|
||||
background: var(--green);
|
||||
}
|
||||
.gh-dot.absent {
|
||||
background: var(--muted);
|
||||
}
|
||||
.gh-status-text.present {
|
||||
color: var(--green);
|
||||
}
|
||||
.gh-status-text.absent {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ma-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin: 0.55rem 0;
|
||||
}
|
||||
.ma-field > span {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ma-field input,
|
||||
.ma-field select {
|
||||
padding: 0.4rem 0.5rem;
|
||||
background: var(--bg-elev);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
}
|
||||
.ma-field input:focus,
|
||||
.ma-field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--purple);
|
||||
}
|
||||
|
||||
.ma-result {
|
||||
margin-top: 0.7rem;
|
||||
font-size: 0.9rem;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
.ma-result.ok {
|
||||
color: var(--green);
|
||||
}
|
||||
.ma-result.err {
|
||||
color: var(--red);
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>hyperhive // CR3D3NTIALS</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="stylesheet" href="/static/colors.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
<link rel="stylesheet" href="/static/common.css" />
|
||||
<link rel="stylesheet" href="/static/credentials.css" />
|
||||
</head>
|
||||
<body class="cred-shell">
|
||||
<!-- Minimal chrome: back link + sub-tab strip, same pattern as
|
||||
logs.html (GITHUB instead of AGENT/INFRA/SYSTEM). Back
|
||||
link points to the H0M3 hub (served at /). -->
|
||||
<header class="page-header">
|
||||
<a class="page-back" href="/">← home</a>
|
||||
<hive-tab-strip
|
||||
class="hive-tabbar cred-tabbar"
|
||||
id="cred-tabbar"
|
||||
prefix="cred"
|
||||
role="tablist"
|
||||
></hive-tab-strip>
|
||||
</header>
|
||||
|
||||
<main class="cred-main">
|
||||
<!-- Agent picker: the selected agent drives the github status. -->
|
||||
<h3>◇ agent</h3>
|
||||
<label class="ma-field">
|
||||
<span>agent</span>
|
||||
<select id="ma-agent"></select>
|
||||
</label>
|
||||
|
||||
<!-- GITHUB tab: single-account PAT paste. No login flow — the
|
||||
operator pastes an existing PAT for a dedicated bot account.
|
||||
Security-warning banner + a link to generate a PAT. -->
|
||||
<section
|
||||
class="cred-pane"
|
||||
id="cred-pane-github"
|
||||
data-tab-pane="github"
|
||||
role="tabpanel"
|
||||
aria-labelledby="cred-tab-github"
|
||||
>
|
||||
<hive-warn level="warning">
|
||||
⚠ use a <strong>dedicated bot account</strong>, not a human's —
|
||||
and a <strong>minimally-scoped</strong> personal access token (only
|
||||
the repos/scopes the agent actually needs, e.g. <code>repo</code> +
|
||||
<code>workflow</code>). the container boundary is the enforcement:
|
||||
anything within the token's scopes is reachable if the agent is ever
|
||||
compromised. the token is injected into the agent's state dir and is
|
||||
<strong>never displayed back</strong> on this page.
|
||||
</hive-warn>
|
||||
|
||||
<h3>◇ status</h3>
|
||||
<div id="gh-status" class="gh-status">
|
||||
<p class="meta">
|
||||
select an agent to see its github credential status.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>◇ provision</h3>
|
||||
<p class="meta">
|
||||
generate a token at
|
||||
<a
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>github.com/settings/tokens</a
|
||||
>
|
||||
and paste it below. one account per agent — pasting a new token
|
||||
replaces the stored one.
|
||||
</p>
|
||||
<form id="gh-form" class="ma-form" autocomplete="off">
|
||||
<label class="ma-field">
|
||||
<span>personal access token</span>
|
||||
<input type="password" name="token" autocomplete="off" required />
|
||||
</label>
|
||||
<button type="submit" class="btn btn-spawn">store token</button>
|
||||
<p id="gh-result" class="ma-result" aria-live="polite"></p>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/static/credentials.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
// CR3D3NTIALS page entry (/credentials.html).
|
||||
//
|
||||
// Operator surface to provision per-agent credentials without editing the
|
||||
// agent's config repo. One sub-tab and an agent picker:
|
||||
// GITHUB — single-account PAT paste against /api/github-account
|
||||
// (GET -> {present}, POST form-encoded {agent, token} ->
|
||||
// {ok:true}; error_response shape on failure).
|
||||
// No account name / homeserver / login mode, and no
|
||||
// live/heartbeat concept for a static PAT — just present/absent.
|
||||
// Per-tab detail comments live next to their section below.
|
||||
|
||||
import { $, esc, renderServerWarnings } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import "@hive/shared/hive-tab-strip.js";
|
||||
import { readApiError, problemMessage } from "@hive/shared/api-error.js";
|
||||
|
||||
let agents = [];
|
||||
|
||||
async function loadState() {
|
||||
try {
|
||||
const resp = await fetch("/api/state");
|
||||
if (!resp.ok) return;
|
||||
const s = await resp.json();
|
||||
renderServerWarnings(s.server_warnings);
|
||||
// `/api/state` exposes the live roster under `containers` (each entry an
|
||||
// object carrying `.name` + `.running`); there is no top-level `agents`
|
||||
// field, so the picker stays compatible with both string + object shapes.
|
||||
const containers = (s.containers || [])
|
||||
.map((a) => (typeof a === "string" ? { name: a } : a))
|
||||
.filter((c) => c && c.name);
|
||||
agents = containers.map((c) => c.name).sort();
|
||||
} catch {
|
||||
// best-effort: on a failed state read the picker renders empty
|
||||
// ("— no agents —") and the submit guard blocks until an agent is
|
||||
// selected, rather than guessing a roster.
|
||||
}
|
||||
}
|
||||
|
||||
function renderAgentPicker() {
|
||||
const sel = $("ma-agent");
|
||||
sel.replaceChildren();
|
||||
if (!agents.length) {
|
||||
sel.append(el("option", { value: "" }, "— no agents —"));
|
||||
return;
|
||||
}
|
||||
sel.append(el("option", { value: "" }, "— select agent —"));
|
||||
for (const a of agents) sel.append(el("option", { value: a }, a));
|
||||
}
|
||||
|
||||
// Shape-agnostic error-body parsing (shared by both tabs' submit handlers)
|
||||
// lives in `@hive/shared/api-error.js` now — `readApiError` +
|
||||
// `problemMessage` (this page only needs the one-line message, not the
|
||||
// full `ApiErrorPanel`; its result lines are single-line `aria-live`
|
||||
// regions, not a swap-in-a-panel context). Was a local function here
|
||||
// originally; promoted so swarm-ui shares the same
|
||||
// shape-agnostic reader instead of each side maintaining its own copy.
|
||||
|
||||
function clearSecrets(formEl) {
|
||||
formEl
|
||||
.querySelectorAll('input[type="password"], input[name="token"]')
|
||||
.forEach((i) => {
|
||||
i.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
// ─── GITHUB tab ─────────────────────────────────────────────────────────
|
||||
|
||||
async function loadGithubStatus(agent) {
|
||||
const status = $("gh-status");
|
||||
if (!agent) {
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "meta" },
|
||||
"select an agent to see its github credential status.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
status.replaceChildren(el("p", { class: "meta" }, "loading…"));
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
"/api/github-account?agent=" + encodeURIComponent(agent),
|
||||
);
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
data = await resp.json();
|
||||
} catch (err) {
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "err" },
|
||||
"could not load status: " +
|
||||
esc(String(err)) +
|
||||
" (the backend endpoint may not be deployed yet).",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const present = !!data.present;
|
||||
status.replaceChildren(
|
||||
el(
|
||||
"div",
|
||||
{ class: "gh-status-line" },
|
||||
el("span", { class: "gh-dot " + (present ? "present" : "absent") }),
|
||||
el(
|
||||
"span",
|
||||
{ class: "gh-status-text " + (present ? "present" : "absent") },
|
||||
present ? "token stored ✓" : "not set",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function submitGithub(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $("gh-result");
|
||||
out.className = "ma-result";
|
||||
out.textContent = "";
|
||||
|
||||
const agent = $("ma-agent").value;
|
||||
if (!agent) {
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "select an agent first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set("agent", agent);
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "storing…";
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/github-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
let body = {};
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* tolerate odd 2xx body */
|
||||
}
|
||||
if (body.ok) {
|
||||
out.className = "ma-result ok";
|
||||
out.textContent = "✓ token stored.";
|
||||
clearSecrets(formEl);
|
||||
loadGithubStatus(agent);
|
||||
} else {
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "✗ store failed (unexpected response).";
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ " + (msg || "store failed (HTTP " + resp.status + ")");
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ request failed: " +
|
||||
String(err) +
|
||||
" (the backend endpoint may not be deployed yet).";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── init ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function onAgentChange(agent) {
|
||||
loadGithubStatus(agent);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await loadState();
|
||||
renderAgentPicker();
|
||||
$("ma-agent").addEventListener("change", (e) =>
|
||||
onAgentChange(e.target.value),
|
||||
);
|
||||
$("gh-form").addEventListener("submit", submitGithub);
|
||||
|
||||
document.getElementById("cred-tabbar").configure({
|
||||
tabs: [{ id: "github", label: "GITHUB" }],
|
||||
defaultId: "github",
|
||||
});
|
||||
|
||||
onAgentChange("");
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
@ -90,16 +90,6 @@
|
|||
<span class="home-tile-desc">kept state · container load</span>
|
||||
</a>
|
||||
|
||||
<a class="home-tile" href="/credentials.html">
|
||||
<span class="home-tile-head">
|
||||
<span class="home-tile-icon" aria-hidden="true">🔑</span>
|
||||
<span class="home-tile-label">Credentials</span>
|
||||
</span>
|
||||
<span class="home-tile-desc"
|
||||
>provision per-agent github + forge accounts</span
|
||||
>
|
||||
</a>
|
||||
|
||||
<!-- API tile: the OpenAPI spec + Swagger UI are always served by
|
||||
hive-c0re itself (docs/web-ui/dashboard.md::Dashboard
|
||||
endpoints), so this tile is never gated/hidden. -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// hive-tab-strip.js — <hive-tab-strip>, the markup-owning tab-strip custom
|
||||
// element behind the logs/credentials/core/builds sub-page tabbars. Owns
|
||||
// element behind the logs/core/builds sub-page tabbars. Owns
|
||||
// rendering the <a class="hive-tab"> markup from a declarative `tabs` list
|
||||
// instead of every page hand-writing the same <nav><a>...</a></nav>
|
||||
// boilerplate, then wires the existing createTabStrip() behaviour
|
||||
|
|
|
|||
109
frontend/packages/swarm-ui/src/pages/LinkGithubAccountForm.tsx
Normal file
109
frontend/packages/swarm-ui/src/pages/LinkGithubAccountForm.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// <LinkGithubAccountForm> — writes a GitHub personal access token for one
|
||||
// agent into the swarm secret store.
|
||||
// PUTs `/api/hives/{hive}/agents/{agent}/github-account` — 204 on success,
|
||||
// 400/500 as `problem+json`, shown via `ApiErrorPanel` like
|
||||
// `LinkForgeAccountForm`.
|
||||
//
|
||||
// One token per agent. A blind set/update: no route says whether a token is
|
||||
// stored, and none hands one back.
|
||||
import { useState } from "preact/hooks";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { TextField } from "../ui/text-field/TextField.js";
|
||||
import { Button } from "../ui/button/Button.js";
|
||||
import "./LinkMatrixAccountForm.css";
|
||||
|
||||
type SubmitState =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "done" }
|
||||
| { status: "error"; problem: ProblemDetails };
|
||||
|
||||
export function LinkGithubAccountForm({
|
||||
hive,
|
||||
agent,
|
||||
onClose,
|
||||
}: {
|
||||
hive: string;
|
||||
agent: string;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [token, setToken] = useState("");
|
||||
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
||||
|
||||
async function submit(e: Event) {
|
||||
e.preventDefault();
|
||||
setResult({ status: "submitting" });
|
||||
try {
|
||||
const r = await fetch(
|
||||
`/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}/github-account`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
},
|
||||
);
|
||||
if (!r.ok) {
|
||||
setResult({ status: "error", problem: await readApiError(r) });
|
||||
return;
|
||||
}
|
||||
setResult({ status: "done" });
|
||||
// The store holds the token now; nothing here needs it.
|
||||
setToken("");
|
||||
} catch (err) {
|
||||
setResult({ status: "error", problem: { detail: String(err) } });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={`link a github account — ${agent}`}
|
||||
icon="🔗"
|
||||
onClose={onClose}
|
||||
>
|
||||
<p>
|
||||
Writes the token to the swarm secret store. The agent fetches it within
|
||||
two minutes, and its <code>gh</code> and <code>git push</code> to
|
||||
github.com then authenticate with it. Use a dedicated bot account and a
|
||||
minimally scoped token, created at{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
github.com/settings/tokens
|
||||
</a>
|
||||
; GitHub notifications also need the <code>notifications</code> scope.
|
||||
</p>
|
||||
<form class="link-matrix-account-form" onSubmit={submit}>
|
||||
<TextField
|
||||
id="github-account-token"
|
||||
label="personal access token"
|
||||
type="password"
|
||||
value={token}
|
||||
required
|
||||
onInput={setToken}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={result.status === "submitting"}
|
||||
>
|
||||
{result.status === "submitting" ? "linking…" : "link account"}
|
||||
</Button>
|
||||
</form>
|
||||
{result.status === "done" && (
|
||||
<p class="link-matrix-account-result-ok">
|
||||
stored a github token for <strong>{agent}</strong>
|
||||
</p>
|
||||
)}
|
||||
{result.status === "error" && (
|
||||
<ApiErrorPanel
|
||||
context="failed to link the account"
|
||||
problem={result.problem}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ import { type TableColumn } from "../../ui/table/Table.js";
|
|||
import { CreateAgentForm } from "../CreateAgentForm.js";
|
||||
import { LinkMatrixAccountForm } from "../LinkMatrixAccountForm.js";
|
||||
import { LinkForgeAccountForm } from "../LinkForgeAccountForm.js";
|
||||
import { LinkGithubAccountForm } from "../LinkGithubAccountForm.js";
|
||||
import { WantedMenu } from "./WantedMenu.js";
|
||||
import "./AgentsPage.css";
|
||||
|
||||
|
|
@ -98,6 +99,8 @@ const VIEW_MODE_KEY = "swarm-ui:agents:view-mode";
|
|||
|
||||
export function AgentsPage() {
|
||||
const [rows, setRows] = useState<AgentRow[] | null>(null);
|
||||
// The row showing the "link a github account" dialog, same shape.
|
||||
const [githubTarget, setGithubTarget] = useState<AgentRow | null>(null);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
|
|
@ -600,6 +603,22 @@ export function AgentsPage() {
|
|||
: "no hive on record for this agent — nothing to link against"
|
||||
}
|
||||
/>
|
||||
<Badge
|
||||
variant="quiet"
|
||||
icon={<LinkIcon />}
|
||||
value="link github account"
|
||||
onClick={
|
||||
detailTarget.hive
|
||||
? () => setGithubTarget(detailTarget)
|
||||
: undefined
|
||||
}
|
||||
disabled={!detailTarget.hive}
|
||||
title={
|
||||
detailTarget.hive
|
||||
? `link a github account to ${detailTarget.name}`
|
||||
: "no hive on record for this agent — nothing to link against"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* MVP scope per mara's own ruling: a small read-only
|
||||
preview, no header/no input — the full terminal
|
||||
|
|
@ -651,6 +670,20 @@ export function AgentsPage() {
|
|||
/>
|
||||
) : null}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={githubTarget !== null}
|
||||
onClose={() => setGithubTarget(null)}
|
||||
label="link a github account"
|
||||
plain
|
||||
>
|
||||
{githubTarget?.hive ? (
|
||||
<LinkGithubAccountForm
|
||||
hive={githubTarget.hive}
|
||||
agent={githubTarget.name}
|
||||
onClose={() => setGithubTarget(null)}
|
||||
/>
|
||||
) : null}
|
||||
</Dialog>
|
||||
<ConfirmDialog
|
||||
open={confirmTarget !== null}
|
||||
label={confirmTarget ? CONFIRM_COPY[confirmTarget.state].label : ""}
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
//! Per-agent GitHub PAT provisioning for the dashboard's GITHUB tab:
|
||||
//! `POST /api/github-account` stores it, `GET /api/github-account` reports
|
||||
//! whether one is stored.
|
||||
|
||||
use axum::extract::{Form, Query};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::{Ident, error_response};
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
/// 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. No account creation and no login modes — the
|
||||
/// operator pastes a PAT for an existing account.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct GithubAccountForm {
|
||||
agent: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
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 }`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/github-account",
|
||||
request_body(content = GithubAccountForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "PAT provisioned", body = GithubAccountResult),
|
||||
(status = 500, description = "invalid agent name, empty token, or the write failed"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Response {
|
||||
let agent = f.agent.trim();
|
||||
let token = f.token.trim();
|
||||
let Ok(agent) = Ident::parse(agent) else {
|
||||
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.as_str(), 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, IntoParams)]
|
||||
pub(super) struct GithubAccountQuery {
|
||||
agent: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
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,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/github-account",
|
||||
params(GithubAccountQuery),
|
||||
responses(
|
||||
(status = 200, description = "whether a github PAT is provisioned", body = GithubAccountStatus),
|
||||
(status = 500, description = "invalid agent name"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn get_github_account(Query(q): Query<GithubAccountQuery>) -> Response {
|
||||
let agent = q.agent.trim();
|
||||
let Ok(agent) = Ident::parse(agent) else {
|
||||
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()
|
||||
}
|
||||
|
|
@ -38,7 +38,6 @@ use crate::lifecycle;
|
|||
(name = "approvals", description = "approve/deny pending approval rows"),
|
||||
(name = "build_logs", description = "build log headers, full rows, and raw text downloads"),
|
||||
(name = "lifecycle_ops", description = "agent container lifecycle: rebuild/restart/start/stop/pause/limits"),
|
||||
(name = "matrix_accounts", description = "github account provisioning for agents"),
|
||||
(name = "meta_inputs", description = "bulk flake-input update for the meta flake"),
|
||||
(name = "misc_api", description = "operator inbox, compose, spawn-request, hive stats"),
|
||||
(name = "permissions", description = "tool-group + capability assignment for agents"),
|
||||
|
|
@ -61,7 +60,6 @@ pub(crate) use hive_types::Ident;
|
|||
mod health;
|
||||
mod journal;
|
||||
mod lifecycle_ops;
|
||||
mod matrix_accounts;
|
||||
mod meta_inputs;
|
||||
mod misc_api;
|
||||
pub(crate) mod permissions;
|
||||
|
|
@ -126,8 +124,7 @@ pub async fn serve(
|
|||
// call below is therefore scoped to exactly one path — two handlers
|
||||
// in the same call only when they genuinely share a path with
|
||||
// different methods (`schedules::api_schedules`/`post_schedule_new`
|
||||
// on `/api/schedules`, `matrix_accounts::get_github_account`/
|
||||
// `post_github_account` on `/api/github-account`) — chained via
|
||||
// on `/api/schedules`) — chained via
|
||||
// repeated `.routes(...)` calls instead of one giant `routes!(...)`
|
||||
// with everything in it.
|
||||
let (router, api) = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
|
||||
|
|
@ -137,10 +134,6 @@ pub async fn serve(
|
|||
.routes(routes!(journal::get_journal_host))
|
||||
.routes(routes!(state_snapshot::api_state))
|
||||
.routes(routes!(state_files::get_state_file))
|
||||
.routes(routes!(
|
||||
matrix_accounts::post_github_account,
|
||||
matrix_accounts::get_github_account
|
||||
))
|
||||
.routes(routes!(misc_api::api_operator_inbox))
|
||||
.routes(routes!(misc_api::api_stats_hive))
|
||||
.routes(routes!(misc_api::api_container_resources))
|
||||
|
|
@ -371,10 +364,6 @@ mod router_build_probe {
|
|||
.routes(routes!(journal::get_journal_host))
|
||||
.routes(routes!(state_snapshot::api_state))
|
||||
.routes(routes!(state_files::get_state_file))
|
||||
.routes(routes!(
|
||||
matrix_accounts::post_github_account,
|
||||
matrix_accounts::get_github_account
|
||||
))
|
||||
.routes(routes!(misc_api::api_operator_inbox))
|
||||
.routes(routes!(misc_api::api_stats_hive))
|
||||
.routes(routes!(misc_api::api_container_resources))
|
||||
|
|
|
|||
|
|
@ -369,25 +369,6 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> {
|
|||
.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, `services.hyperhive.agent.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?)
|
||||
}
|
||||
|
||||
/// Register the hive-ci Forgejo Actions runner: hand the freshly-minted
|
||||
/// registration token to hive-priv, which writes it to the host-side
|
||||
/// `/run/hive-ci/runner-token` env-file and restarts the in-container runner.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue