Merge remote-tracking branch 'forge/main' into docs/networking-scheduler-pass
# Conflicts: # docs/networking/gateway.md
This commit is contained in:
commit
f63ab954c9
57 changed files with 1042 additions and 1070 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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
|
|||
| --- | --- | --- |
|
||||
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-ui.enable` |
|
||||
| `auth.<swarm>/` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` |
|
||||
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.behindGateway` |
|
||||
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` |
|
||||
| `chat.<swarm>/_matrix/*` | tuwunel (`8008`) | `hive-matrix.nix`, `swarm.matrix.gatewayHost != null` |
|
||||
| `chat.<swarm>/` | fluffychat-web static (404 with the GUI off) | `hive-matrix.nix`, `deploy.matrix.gui.enable` |
|
||||
| `chat.<swarm>/config.json` | inline JSON (FluffyChat boot config) | `hive-matrix.nix`, `deploy.matrix.gui.enable` |
|
||||
|
|
@ -170,7 +170,7 @@ services.hyperhive.deploy.forgejo.openFirewall = false; # default
|
|||
|
||||
`sshPort` serves `git clone/push/pull` over SSH (`git@<domain>:owner/repo.git` with `-p 2222`). SSH goes straight to Forgejo, not through nginx.
|
||||
|
||||
`openFirewall` (default `false`) opens `httpPort` and `sshPort` on the host. Agents don't need it — they come through the gateway. Set it for a browser reaching `http://<host>:<httpPort>/` directly, or for external git clients pushing over SSH. The forge vhost behind the gateway (`deploy.forgejo.behindGateway`, default `true`) needs only the gateway's own `openFirewall`.
|
||||
`openFirewall` (default `false`) opens `httpPort` and `sshPort` on the host. Agents don't need it — they come through the gateway. Set it for a browser reaching `http://<host>:<httpPort>/` directly, or for external git clients pushing over SSH. Forge's gateway vhost doesn't need `openFirewall` — the gateway's own `openFirewall` option covers that path.
|
||||
|
||||
### `rootUrl` override
|
||||
|
||||
|
|
@ -178,14 +178,7 @@ services.hyperhive.deploy.forgejo.openFirewall = false; # default
|
|||
services.hyperhive.swarm.forge.rootUrl = "https://forge.example.com/";
|
||||
```
|
||||
|
||||
`rootUrl` (default `null`) overrides the Forgejo `ROOT_URL` derived from `forge.domain` and the gateway:
|
||||
|
||||
| Shape | Derived `ROOT_URL` |
|
||||
|---|---|
|
||||
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (no port suffix when `gateway.httpsPort == 443`) |
|
||||
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
||||
|
||||
Set it when `forge.domain` differs from the public URL, or for a bespoke shape such as an external reverse proxy on another host or path. It must end with `/` (an assertion enforces this).
|
||||
`rootUrl` (default `null`) overrides the Forgejo `ROOT_URL` that's autoderived as `https://<forge.domain>/`, with `:<gateway.httpsPort>` appended when that port isn't 443. The gateway always terminates TLS, so the forge is always advertised over `https://`. Set `rootUrl` explicitly when `forge.domain` differs from the public URL, or for a bespoke shape such as an external reverse proxy on another host or path. It must end with `/` (an assertion enforces this).
|
||||
|
||||
## Security headers
|
||||
|
||||
|
|
@ -218,7 +211,7 @@ When enabled, every vhost adds `Strict-Transport-Security: max-age=…[; include
|
|||
`services.hyperhive.gateway.localHostsEntry = true` maps to `127.0.0.1` in the host's `/etc/hosts`:
|
||||
|
||||
- the hive domain;
|
||||
- every name a module on this host contributes to `gateway.localNames` — each swarm service this host runs adds its own (`forge.<swarm>` when behind the gateway, `chat.<swarm>`, `auth.<swarm>`, the swarm UI's apex, …).
|
||||
- every name a module on this host contributes to `gateway.localNames` — each swarm service this host runs adds its own (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`, the swarm UI's apex, …).
|
||||
|
||||
`services.hyperhive.deploy.singleHostSwarm` turns it on. Leave it off with real DNS.
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ build can't hold the runner's single slot indefinitely).
|
|||
|
||||
## Container design
|
||||
|
||||
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `deploy.forgejo.behindGateway = true`.
|
||||
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI.
|
||||
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
|
||||
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
|
||||
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.
|
||||
|
|
|
|||
|
|
@ -42,10 +42,6 @@ to the store under its own name, pulling what it needs when it needs it. No
|
|||
process reads a secret on another principal's behalf: the principal that
|
||||
needs a value is the principal that authenticates for it.
|
||||
|
||||
One per-agent credential file — the github token — sits outside this page:
|
||||
it's operator-supplied and never passes through the store, so the table
|
||||
below doesn't govern it.
|
||||
|
||||
**Per secret, the target specifies minter, reader, and renewal strategy.**
|
||||
Those three are the contract, and the reader is a process pulling a store
|
||||
path at runtime — not a path on disk, and not a unit whose job is to turn a
|
||||
|
|
@ -69,6 +65,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
|
||||
|
|
@ -296,6 +272,6 @@ note, not an error.
|
|||
|
||||
A surface has no URL when it isn't browser-reachable: `home` needs
|
||||
`services.hyperhive.domain`; `forge` needs
|
||||
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
|
||||
`services.hyperhive.swarm.forge.publicUrl` (set by default); `matrix` needs
|
||||
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command
|
||||
exits with a hint naming the option to set.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ what you actually do here.
|
|||
|
||||
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
|
||||
linking to every surface (Dashboard, Flow, Logs, Builds, Stats,
|
||||
Settings, Core, Credentials). Every page links back to H0M3, so you're
|
||||
Settings, Core). Every page links back to H0M3, so you're
|
||||
never more than one step from the hub.
|
||||
|
||||
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
|
||||
|
|
@ -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,11 +1,10 @@
|
|||
// api-error.ts — reads a failed fetch `Response` into a `ProblemDetails`
|
||||
// (RFC 9457, `application/problem+json`) object, shape-agnostically.
|
||||
//
|
||||
// Promoted from `dashboard/src/credentials.js`'s original `readErrorBody`,
|
||||
// which returned a flat message string. This returns the structured object
|
||||
// instead so a caller — chiefly `ApiErrorPanel` (./api-error-panel/) — can
|
||||
// render title/status/detail separately and build a useful copy-button
|
||||
// payload, rather than re-parsing a pre-squashed string.
|
||||
// Returns the structured object (rather than a flat message string) so a
|
||||
// caller — chiefly `ApiErrorPanel` (./api-error-panel/) — can render
|
||||
// title/status/detail separately and build a useful copy-button payload,
|
||||
// rather than re-parsing a pre-squashed string.
|
||||
//
|
||||
// RFC 9457 is this hive's committed error-body contract for first-party
|
||||
// APIs (mara: "any api of our own responding with error that is not rfc
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
// hive-warn.js — <hive-warn>, the shared inline warning-banner
|
||||
// component. Consolidates three independently-written instances of the
|
||||
// same thing: `.cred-warning` (credentials.html, static
|
||||
// markup), `.tombstone-warn` (core.js, JS-built), `.port-conflict`
|
||||
// (swarm.js, JS-built) — the first two differed only by an
|
||||
// component. Consolidates what were independently-written instances of
|
||||
// the same thing, among them `.tombstone-warn` (core.js, JS-built) and
|
||||
// `.port-conflict` (swarm.js, JS-built) — some differed only by an
|
||||
// undeliberate 10% vs 8% tint, the strongest evidence this was drift,
|
||||
// not three genuinely different needs.
|
||||
// not genuinely different needs.
|
||||
//
|
||||
// Purely presentational — no internal state, no lifecycle beyond
|
||||
// attaching its shadow root once. `level` ('info' | 'warning' | 'error',
|
||||
|
|
|
|||
|
|
@ -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; 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>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue