Commit graph

1,904 commits

Author SHA1 Message Date
damocles
1ebc384e66 dashboard: GET /api/matrix-accounts — list an agent's provisioned matrix accounts (#1698) 2026-06-16 10:29:17 +02:00
iris
06d57a61f8 dashboard: fix matrix-accounts GET path to /api/matrix-accounts
BE-1 (the list endpoint) lives at GET /api/matrix-accounts (reads go under
/api/, matching /api/state and /api/operator-inbox); the page was calling
the un-prefixed /matrix-accounts. Correct the fetch + the contract comment +
the docs reference. POST /matrix-account-login (mutation, root path like
/approve) is unchanged.
2026-06-16 10:22:53 +02:00
iris
8bb3bd82a1 dashboard: drop matrix-accounts experimental notice; relabel list as provisioned
Per-account token-failure isolation is now live on the matrix daemon, so a
bad credential entered via this form can no longer take down the agent's
whole matrix session — remove the experimental notice (+ its now-unused CSS).

Also relabel the account list 'configured accounts' -> 'provisioned accounts'
to match the v1 read path (lists accounts with a stored token; a
config-declared-but-unprovisioned account appears once provisioned through
the form). Docs + module header updated to match.
2026-06-16 10:22:53 +02:00
iris
9293fe3ac9 dashboard: matrix-accounts page — provision/login per-agent external matrix accounts
New standalone H0M3 page (/matrix-accounts.html) and tile. An agent
picker drives a list of that agent's configured matrix accounts (name,
homeserver, token-stored status) and a provision form that logs in by
password or stores an existing token.

Frontend half of the per-agent external matrix-account provisioning
work. Built against the v1 backend contract:
  GET  /matrix-accounts?agent=<name>
       -> { accounts: [ { name, homeserver, token_present } ] }
  POST /matrix-account-login  (x-www-form-urlencoded, operator-auth)
       fields: agent, account, homeserver, mode=password|token,
               user_id?, password?, token?
       -> 2xx { ok, user_id } | 4xx { error }

The token is never echoed back; secret inputs are cleared on submit.
Token-status dot reflects token-stored, not live session (a true
up/down indicator needs the daemon account registry, a follow-up). The
form carries an experimental notice pending per-account failure
isolation on the matrix daemon.

Blocked from merge on the backend endpoints and the daemon
failure-isolation fix; opening for review + to pin the UI/backend wire
contract.
2026-06-16 10:22:53 +02:00
iris
c187366961 dashboard: dedent call.js moved blocks to module top level
Cosmetic follow-up to the Y3R C4LL extraction (argus nit on #1705): the
approvals + questions sections kept their original 2-space IIFE indentation
from tabs.js, which read as if they were inside a block and was inconsistent
with the unindented operator-inbox section + the other domain modules.
Uniform dedent — no logic change, build unaffected.
2026-06-16 10:22:53 +02:00
iris
a0fc7e1a9f dashboard: extract questions domain into call.js — Y3R C4LL split complete
Final domain: move the operator question queue (questionRowCache, the
sync/apply/filter/fingerprint/buildQuestionLi/renderQuestions fns, formatTtl,
and the .q-ttl countdown ticker) from tabs.js into call.js. tabs.js imports
syncQuestionsFromSnapshot, applyQuestionAdded, applyQuestionResolved,
renderQuestions, and the activeQuestionCount getter; refreshTabCounts now
sums activeApprovalCount()+activeQuestionCount()+operatorInboxCount().

The .status-age (SW4RM) and .approval-ts/.reminder-due/.sched-due
(approvals+schedules) tickers stay in tabs.js — only the questions-specific
.q-ttl ticker moved. questionsState stays in state.js (shared with the SW4RM
badges). Behaviour-preserving; no visual change.

Net: tabs.js 2323 to 1603 lines; the Y3R C4LL pane now lives in call.js,
alongside permissions.js / schedules.js as a per-domain module.
2026-06-16 10:22:53 +02:00
iris
d0ec0d2896 dashboard: extract operator-inbox + approvals into call.js (#1451)
Begin splitting the tabs.js monolith: lift the Y3R C4LL domain into a new
call.js module (mirrors permissions.js / schedules.js).

- questionsState + QUESTION_HISTORY_LIMIT move to state.js: they're read by
  both the SW4RM container rows (per-agent question-count badges) and the
  Y3R C4LL questions domain, so they're cross-domain shared state.
- operator-inbox domain (state + refresh/render/mark/append) → call.js.
- approvals domain (state + sync/apply/render/diff-panel/history) → call.js.
- call.js exposes count getters (activeApprovalCount, operatorInboxCount);
  the entry's refreshTabCounts pulls them. Live-mutation paths call an
  injected onCountsChanged callback (registered via initCall at boot)
  instead of reaching into the coordinator (avoids a circular import).
- renderPeerHives, physically interleaved in the moved range but part of
  the SW4RM/peers domain, stays in tabs.js.

Behaviour-preserving; no visual change. Questions domain follows next.

(Recreated after a harness-restart git-object corruption; identical content.)
2026-06-16 10:22:53 +02:00
atlas
e6f9679ec9 hive-forge: add issues/issue-list discovery aliases for the list verb
The verb that lists issues/PRs with filters is `list`, but `issues` and
`issue-list` are the names people reach for first — and clap's
"did you mean" tip points at `issue` / `issue-create`, never at `list`.
Two agents independently failed to find it that way.

Add `issues` and `issue-list` as visible clap aliases so the verb is
discoverable under the names people actually type (and they show in
`--help`). No behaviour change; `list` stays canonical.
2026-06-16 00:18:32 +02:00
damocles
e5cd4e58f1 matrix multi-account: isolate per-account stale-token failure so a bad secondary token doesn't kill the daemon 2026-06-15 23:13:45 +02:00
atlas
c27f915e47 fix(meta): forward HIVE_FORGE_URL etc. into globalEnvironment
The forwarded host env vars (HIVE_FORGE_URL, HIVE_FORGE_PUBLIC_URL,
HYPERHIVE_HIVE_DOMAIN, ...) were emitted only into the harness
service's own unit environment. Under network isolation that left the
bash-task runner, the matrix daemon, tea-login and interactive shells
without HIVE_FORGE_URL, so `hive-forge` and friends fell back to the
loopback default (localhost:3000) — unreachable from a private netns,
where the in-cluster gateway URL (forge.<domain>) is the only path.

Emit the forwarded set into systemd.globalEnvironment too, which every
unit + shell in the container inherits, so all in-container surfaces
see the same forge/matrix endpoints. The existing harness-service
forward is kept (harmless redundancy). Shared netns is unaffected (the
localhost default still works there).

Adds a regression test asserting the forwarded vars land inside the
globalEnvironment block, and the generated flake parses.
2026-06-15 22:04:43 +02:00
damocles
f7199f183c drop issue number from source comment (tracker-tag lint) 2026-06-15 21:55:55 +02:00
damocles
354b270d2a matrixAccounts url: single-source-of-truth default binding (addresses review) 2026-06-15 21:48:51 +02:00
damocles
d15ecff6d2 isolation: forward HIVE_MATRIX_URL so matrix daemon reaches the gateway 2026-06-15 21:38:34 +02:00
lexis
314fb8298f docs: add agent.nix hyperhive.matrixAccounts multi-account matrix configuration (follow-up to #1692) 2026-06-15 21:28:36 +02:00
damocles
8ba0abcf27 matrixAccounts: assert no extra named 'main' (reserved for hive account) 2026-06-15 21:24:16 +02:00
damocles
36fb041c55 matrixAccounts: hive 'main' is implicit primary, option declares extras only 2026-06-15 21:24:16 +02:00
damocles
63db108516 rework matrixAccounts to attrset keyed by name + matrixPrimaryAccount 2026-06-15 21:24:16 +02:00
damocles
dcc059b4b8 feat: hyperhive.matrixAccounts nix option (matrix multi-account layer 2) 2026-06-15 21:24:16 +02:00
atlas
26d81cd36e fix(harness): stop resolvconf/dhcpcd clobbering the bridge DNS resolver
Under network isolation, agents lost DNS resolution of the hive's own
zones (forge.<domain>, matrix.<domain>): the hyperhive-isolated-dns
oneshot points resolv.conf at the bridge resolver, but resolvconf
regenerated it from host-tracking right after (dhcpcd re-triggering that
when the veth comes up), reverting to the non-authoritative host
resolver. Agents then couldn't reach the in-hive forge or matrix through
the gateway, which broke an isolate-by-default rollout.

Take resolvconf and dhcpcd out of the resolv.conf loop: disable
resolvconf and tell dhcpcd not to touch resolv.conf (without disabling
dhcpcd itself, so the veth still gets its address). Then the last writer
owns it -- the nixos-container host-copy in shared netns, or the oneshot
in isolated mode. Mirrors the approach the matrix container already
takes. Eval-checked; flake check passes.
2026-06-15 20:45:20 +02:00
damocles
3b26f58a00 docs: note ping arm in dispatch_op is unreachable 2026-06-15 20:28:31 +02:00
damocles
8e79eb4f26 feat: multi-account matrix daemon (account-routed mcp surface) 2026-06-15 20:23:42 +02:00
damocles
73c53c7d4a feat(#1685): surface attachments in read_room + add download_file 2026-06-15 18:17:35 +02:00
atlas
6490fc422e feat(hive-forge): add artifact-get verb to download CI run artifacts
Forgejo 15 has no REST endpoint to download an Actions artifact — the
only path is the web UI download route, which is keyed by the run's
global id rather than the per-repo run number shown in run-page URLs.
The REST artifacts list route keys off the run number instead, so the
two can't be chained directly.

artifact-get takes the run number (what pr-status surfaces as a CI
context target_url), translates it to the global run id via the REST
runs list by matching each run's html_url tail, then GETs the web
download route with the agent's forge token. Saves the artifact zip to
a path (default /tmp/forge-artifact-<name>.zip) or streams to stdout
with -o -.

The artifact name is percent-encoded into the path. The encoder that
list already used for query-string filters is promoted to a shared
verbs::pct_encode helper so both call sites stay in sync.

Lets an agent pull a CI-built artifact (e.g. a paper PDF) into /shared
without host access.
2026-06-15 17:59:38 +02:00
damocles
801b886a4c drop stale send_file_dm doc refs (per review) 2026-06-15 17:59:02 +02:00
damocles
07941421c1 replace send_file_dm with open_dm resolver (per review) 2026-06-15 17:59:02 +02:00
damocles
19f1fa4f28 rustfmt send_file handlers 2026-06-15 17:59:02 +02:00
damocles
cbbd6f3d6e feat(#1681): add matrix send_file + send_file_dm tools 2026-06-15 17:59:02 +02:00
damocles
1b0f035a05 matrix: add #[must_use] on room_label per review 2026-06-15 15:50:53 +02:00
damocles
1b1c6e03b9 refactor(matrix): extract room_label helper, dedup 4 call sites 2026-06-15 15:50:53 +02:00
atlas
9902d9d9a5 fix(ci): resolve the forge domain to loopback in the hive-ci runner
Forgejo's Actions artifact API hands the runner an upload URL built from
forgejo's ROOT_URL — the public forge domain (forge.<domain>), not the
runner's 127.0.0.1:<httpPort> registration URL. The hive-ci container shares
host netns but has no resolver entry for the hive's own domains (the
gateway's localHostsEntry only touches the host), so actions/upload-artifact
fails with 'getaddrinfo ENOTFOUND forge.<domain>' while normal CI (checkout
via the localhost registration URL) is unaffected. Add a networking.extraHosts
entry mapping the forge domain to loopback so the upload reaches the local
forge via the gateway.
2026-06-15 13:32:01 +02:00
atlas
f633abbdc4 refactor(hive-forge): share latest-per-reviewer logic between pr-status and pr-merge
pr-status and pr-merge both computed 'latest non-comment review verdict per
reviewer' independently (identical oldest-first, COMMENT/PENDING-skipping,
supersede-by-later loop). Extract it to a shared verbs::latest_reviews helper
so the verdict semantics live in one place and can't drift between the
health view and the pre-merge changes-requested gate. Pure dedup, no
behaviour change.
2026-06-15 11:44:19 +02:00
damocles
f6e1651dff feat(#1671): reject matrix send when room has unread messages 2026-06-15 11:38:25 +02:00
damocles
d89666baa9 docs(ci): document jobTimeout, fix forge.ci option path 2026-06-15 11:37:50 +02:00
atlas
bfeb23ce06 fix(forge): pre-create Actions artifact storage dir so uploads persist
Forgejo's local Actions-artifact storage defaults to
/var/lib/forgejo/data/actions_artifacts but Forgejo does not pre-create it.
The artifact endpoint ingests the chunked upload, then the merge-chunks step
fails with 'lstat .../actions_artifacts/tmpNNN: no such file or directory',
so every upload-artifact step dies after a successful build. Pre-create the
dir (forgejo-owned) via tmpfiles. actions.ENABLED already registers the
endpoints; this gives them somewhere to write.
2026-06-15 11:37:33 +02:00
atlas
9d8367bb24 feat(hive-forge): add pr-merge verb (#1670)
Adds `hive-forge pr-merge <n> [--method merge|rebase] [--keep-branch]
[--force]` wrapping POST /repos/{owner}/{repo}/pulls/{n}/merge, so agents on
the peer-review-and-merge workflow have a CLI path instead of the raw API.

- Methods: merge (default) | rebase. Squash is intentionally not offered.
- Deletes the head branch after merge unless --keep-branch.
- Safe by default: refuses unless the PR is mergeable, CI is not red, and no
  review's current verdict requests changes (latest-per-reviewer wins, so a
  later approval clears an earlier request-changes). --force overrides and
  also sets Forgejo's force_merge.
- New client helper post_no_content for the 200-empty-body merge response.
2026-06-15 11:36:17 +02:00
iris
06c5d68071 refactor(dashboard): move SYST3M section CSS into shared system-sections.css
The C0R3 page (/core.html) pulled the four SYST3M panel styles (rebuild
queue, meta inputs, container load) by @importing the whole ~39kb
dashboard.css, dragging the entire dashboard stylesheet into the C0R3
CSS bundle.

Extract those section-internal rules into a new system-sections.css and
@import it from both core.css and dashboard.css. core.html now loads only
the small shared file (its CSS bundle drops from ~40kb to 5.3kb) while
the dashboard keeps the rules — a couple are still referenced from
dashboard JS (the schedules view uses .rqe-source*). The container-load
table's .hive-stats-table already lives in common.css, linked directly
by core.html, so it is unaffected.

Behaviour-preserving: identical rules, same custom properties; the moved
selectors are uniquely named to these panels and not redefined
elsewhere, so the cascade is unchanged. Verified the built bundles:
core.css carries the section rules and no dashboard chrome; dashboard.css
still carries them via the import.
2026-06-15 09:54:55 +02:00
damocles
ca84cf427b fix(#1666): wake on pending matrix invites via post-sync sweep 2026-06-14 23:37:45 +02:00
atlas
c7ffe9336b docs(hive-forge): add # Errors to put_no_content (argus review) 2026-06-14 22:09:31 +02:00
atlas
3790faf318 feat(hive-forge): add repo-create + repo-add-collaborator verbs 2026-06-14 22:09:31 +02:00
damocles
41c4682f7a refactor(#1450): lift shared harness-path resolution into hive-sh4re 2026-06-14 21:34:58 +02:00
damocles
789ecd86f6 fix(#1661): show default-perms agents with effective values in perms tab 2026-06-14 21:20:03 +02:00
iris
24cbafc64a feat(dashboard): live-append the LOGS AUDIT tab via audit_entry_added
Completes the audit log end-to-end. The backend now emits an
audit_entry_added event on /dashboard/stream per privileged action (the
new row flattened at top level). logs.js already subscribes to that stream
for the BUILD tab; add a branch that prepends the new row to the AUDIT
table's cached list (de-duped by id against the cold fetch), bumps the
'latest N of M' total, and re-renders while the AUDIT tab is in view — so
a restart shows up without a manual refresh. Docs updated to match.
2026-06-14 20:23:54 +02:00
atlas
f21a812f5f audit-log: #[must_use] on record() per review
argus: record() is pub and returns Option<AuditEntry>; the hive rule
requires #[must_use] on public fns returning a non-trivial value. Call
sites already consume the return (handle_restart_infra via if-let, tests
via let _).
2026-06-13 17:17:00 +02:00
atlas
629f08a113 feat(audit): live-append event for the dashboard audit view
Follow-up to the audit-log backend + surface. Emits a DashboardEvent on
each agent-initiated privileged action so the audit view live-appends off
/dashboard/stream instead of polling.

- new DashboardEvent::AuditEntryAdded { seq, <flattened AuditEntry> } —
  serde tag `audit_entry_added`; the AuditEntry fields flatten to the top
  level so the wire shape matches one /api/audit-log `entries` row exactly.
- Coordinator::emit_audit_entry helper (stamps seq like the others).
- audit_log::record now returns the canonical inserted AuditEntry (id + ts
  assigned) so the streamed event is the same row that was stored — no
  drift. Best-effort unchanged (None on a sqlite blip).
- handle_restart_infra records + emits for every attempt (ok/err/denied),
  threading the coordinator through.

Tests: kind_tag round-trip now covers the new variant; added a flatten
test pinning the top-level wire shape (kind/seq/id/…/detail, no nesting).

Pairs with iris's audit view (the /dashboard/stream listener half).
2026-06-13 17:17:00 +02:00
damocles
9bd51a7440 feat(#1653): optional name param for bash run tool 2026-06-13 17:16:36 +02:00
iris
0b6a52e81f docs(web-ui): document the LOGS AUDIT sub-tab + /api/audit-log
The LOGS page gained a fourth sub-tab (AUDIT) surfacing the
agent-initiated privileged-action audit trail. Document the sub-tab
(lazy fetch, filterable table, outcome/denied badge colouring, the
latest-N-of-total clamp, 30s relative-time ticker) alongside BUILD /
AGENT / SYSTEM, add #audit to the hash-routing list, and add the
GET /api/audit-log endpoint to the API reference.
2026-06-13 17:16:06 +02:00
damocles
e08122a206 fix(#1643): filter destroyed-agent targets from dashboard schedule view 2026-06-13 15:58:44 +02:00
iris
8b991b2cc5 feat(dashboard): AUD1T — privileged-actions audit trail as a LOGS sub-tab
Adds an AUDIT sub-tab to /logs.html (alongside BUILD / AGENT / SYSTEM),
consuming GET /api/audit-log ({ entries, total }). A read-only filterable
table: when / agent / action / target / outcome / detail, newest-first,
with a 'latest 500 of N' header from total and a client-side substring
filter. Outcome badges colour ok green / err red, with an err whose detail
starts 'denied:' rendered amber + labelled 'denied' (capability refusal
reads distinct from an execution failure). Lazy-fetched on tab show (like
SYSTEM); a 30s ticker keeps the relative timestamps honest.

The audit_log store + endpoint landed in the prior audit-log backend work;
this is the operator-visible surface for it. Resolves #1647.
2026-06-13 15:31:50 +02:00
atlas
79fa4f97b7 audit-log: add # Errors doc sections per review
argus review: the four public Result-returning methods (open, list_recent,
count_total, vacuum) need # Errors sections per the hive Rust doc rules;
open had no doc comment. Added all four.
2026-06-13 14:00:02 +02:00
atlas
962259a8d6 audit-log: expose total count in /api/audit-log response
Per iris's dashboard-side ask: return { entries, total } instead of a
bare array so the UI can show 'latest 500 of N' rather than silently
capping at the clamp. Adds AuditLog::count_total().
2026-06-13 13:49:54 +02:00