Add hephaestus sync hub to indri (launchagent, PWA, device-code OIDC) (#369)
Makes indri the canonical **heph** hub for the hub-and-spoke task/context system, deployed as a self-updating LaunchAgent managed by Ansible. Other devices (gilbert) attach as offline-capable spokes.
## What's here
- **`ansible/roles/heph`** (tag `heph`) — bootstrap `cargo install hephd` (only if absent; `--self-update` keeps it current after), version-pinned `heph-pwa` checkout served via `--web-root`, launchagent `mcquack.eblume.heph`:
```
hephd --mode server --http-addr 0.0.0.0:8787 --db … --web-root …
--oidc-issuer …/o/heph/ --oidc-audience heph
--self-update --self-update-interval-secs 600
```
`~/.cargo/bin` is on the agent `PATH` so self-update's `cargo install` works.
- **Caddy** — `heph.ops.eblu.me → localhost:8787` (TLS for the PWA secure context).
- **Authentik** — new `heph` **public device-code** OIDC app + `default-device-code-flow` bound to the default brand's `flow_device_code` (verified live: brand `authentik-default`, field currently unset → additive).
- **Docs** — `services/hephaestus.md` (Path-A seeding runbook + spoke caveat), `indri.md`, changelog fragment.
## Three features requested
- **Autoupdate** — 10-min interval (`--self-update-interval-secs 600`).
- **PWA** — `--web-root` (confirmed shipped in v1.2.0).
- **Spoke** — gilbert reconfig documented (post-merge step).
## Deploy plan (not done yet — awaiting review)
1. Seed from gilbert (Path A): `heph daemon stop` → copy `heph.db` → `DELETE FROM meta WHERE key='origin'`.
2. Sync Authentik `apps`/blueprint; verify blueprint status via API (not just logs).
3. `provision-indri --tags heph,caddy` from this branch.
4. Point gilbert at the hub + `heph auth login`.
## Known follow-ups (heph-side, tracked in the Hephaestus project)
- `heph daemon` can't bake hub/spoke config or pass `--self-update-interval-secs` → worked around by the ansible plist.
- Path-A seeding lacks a clean `hephd --owner-id`/seed command → manual `meta.origin` reset for now.
- Self-update moves hephd ahead of the ansible-pinned PWA shell over time (drift; tolerated by the SW cache, revisit on next release).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #369
This commit is contained in:
parent
f6c926f1f5
commit
a2f1e06224
10 changed files with 403 additions and 0 deletions
|
|
@ -260,5 +260,7 @@
|
|||
tags: cv
|
||||
- role: docs
|
||||
tags: docs
|
||||
- role: heph
|
||||
tags: heph
|
||||
- role: caddy
|
||||
tags: caddy
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ caddy_services:
|
|||
- name: devpi
|
||||
host: "pypi.{{ caddy_domain }}"
|
||||
backend: "http://localhost:3141"
|
||||
- name: heph
|
||||
host: "heph.{{ caddy_domain }}"
|
||||
backend: "http://localhost:8787" # hephaestus hub (server mode) + PWA shell
|
||||
- name: kiwix
|
||||
host: "kiwix.{{ caddy_domain }}"
|
||||
backend: "https://kiwix.tail8d86e.ts.net"
|
||||
|
|
|
|||
49
ansible/roles/heph/defaults/main.yml
Normal file
49
ansible/roles/heph/defaults/main.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
# hephaestus hub — the canonical heph replica (server mode) on indri.
|
||||
# Other devices (e.g. gilbert) are spokes that sync against this hub.
|
||||
# See [[set-up-sync-hub]] and [[host-heph-pwa]] in the hephaestus repo.
|
||||
|
||||
# Pinned release used for the initial `cargo install` and the PWA shell.
|
||||
# After bootstrap, hephd's own --self-update keeps the binary current; this
|
||||
# pin only governs the first install and the bundled PWA shell version.
|
||||
heph_version: v1.2.0
|
||||
|
||||
# Anonymous public HTTPS clone — matches hephd's INSTALL_GIT_URL so the initial
|
||||
# install and unattended self-update build from the same source (no ssh-agent).
|
||||
heph_repo_url: https://forge.eblu.me/eblume/hephaestus.git
|
||||
|
||||
heph_bin_dir: /Users/erichblume/.cargo/bin
|
||||
heph_binary: "{{ heph_bin_dir }}/hephd"
|
||||
|
||||
# rustc/cargo here are rustup shims. The bare (non-mise) environment that the
|
||||
# launchagent and ansible run in falls back to rustup's *default* toolchain,
|
||||
# which can lag behind heph's rust-version floor (Cargo.toml: 1.89). Pin the
|
||||
# channel explicitly so both the bootstrap build and unattended self-update
|
||||
# always use a current toolchain regardless of the host's rustup default.
|
||||
heph_rust_toolchain: stable
|
||||
|
||||
heph_data_dir: /Users/erichblume/.local/share/heph
|
||||
heph_db: "{{ heph_data_dir }}/heph.db"
|
||||
heph_socket: "{{ heph_data_dir }}/hephd.sock"
|
||||
heph_log_dir: /Users/erichblume/Library/Logs
|
||||
|
||||
# Version-pinned source checkout; the PWA static shell is served directly from
|
||||
# its heph-pwa/ subdir (no copy), keeping shell and hub in lockstep at heph_version.
|
||||
heph_pwa_src_dir: /Users/erichblume/.cache/heph-pwa-src
|
||||
heph_web_root: "{{ heph_pwa_src_dir }}/heph-pwa"
|
||||
|
||||
# Hub listens on all interfaces so tailnet spokes can reach it directly
|
||||
# (http://indri.tail8d86e.ts.net:8787) and Caddy can proxy heph.ops.eblu.me.
|
||||
# Access is gated by Authentik OIDC regardless — tailnet reachability is not
|
||||
# enough (this is the owner's most sensitive data).
|
||||
heph_http_addr: 0.0.0.0:8787
|
||||
heph_port: 8787
|
||||
heph_external_url: https://heph.ops.eblu.me
|
||||
|
||||
# Authentik OIDC — issuer + audience together turn hub auth on. The audience is
|
||||
# the device-code client id (see argocd/manifests/authentik heph blueprint).
|
||||
heph_oidc_issuer: https://authentik.ops.eblu.me/application/o/heph/
|
||||
heph_oidc_audience: heph
|
||||
|
||||
# Self-update poll interval (seconds). 10 minutes.
|
||||
heph_self_update_interval_secs: 600
|
||||
6
ansible/roles/heph/handlers/main.yml
Normal file
6
ansible/roles/heph/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Restart heph
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.heph.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
changed_when: true
|
||||
82
ansible/roles/heph/tasks/main.yml
Normal file
82
ansible/roles/heph/tasks/main.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
# hephaestus hub (server mode) on indri.
|
||||
#
|
||||
# DATA SEEDING (one-time, Path A — do this BEFORE the first provision so the hub
|
||||
# adopts gilbert's existing data instead of being born empty):
|
||||
#
|
||||
# 1. On the seed device (gilbert): heph daemon stop
|
||||
# 2. Copy its store to indri: scp ~/.local/share/heph/heph.db \
|
||||
# indri:~/.local/share/heph/heph.db
|
||||
# 3. On indri, give the hub its OWN device origin (keeps gilbert's owner_id +
|
||||
# data; hephd regenerates a fresh origin on next start when it is missing):
|
||||
# sqlite3 ~/.local/share/heph/heph.db "DELETE FROM meta WHERE key='origin';"
|
||||
# 4. Run this role (installs hephd, stages the PWA, loads the launchagent).
|
||||
#
|
||||
# hephd auto-creates an empty store on first start if none exists, so seeding is
|
||||
# optional — skip it only if you intend a fresh, empty hub.
|
||||
|
||||
- name: Ensure heph data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ heph_data_dir }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
|
||||
- name: Check for installed hephd binary
|
||||
ansible.builtin.stat:
|
||||
path: "{{ heph_binary }}"
|
||||
register: heph_binary_stat
|
||||
|
||||
# Bootstrap install only when hephd is absent. Thereafter hephd's own
|
||||
# --self-update keeps it current; ansible must not fight (or downgrade) it.
|
||||
# This builds from source and can take several minutes on a cold cargo cache.
|
||||
- name: Bootstrap-install heph + hephd from the forge ({{ heph_version }})
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
{{ heph_bin_dir }}/cargo install --locked
|
||||
--git {{ heph_repo_url }}
|
||||
--tag {{ heph_version }}
|
||||
heph hephd
|
||||
environment:
|
||||
PATH: "{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
RUSTUP_TOOLCHAIN: "{{ heph_rust_toolchain }}"
|
||||
when: not heph_binary_stat.stat.exists
|
||||
changed_when: true
|
||||
notify: Restart heph
|
||||
|
||||
# Checkout provides the PWA shell at {{ heph_web_root }} (heph-pwa/ subdir),
|
||||
# served directly by hephd. Static files are read from disk per request, so a
|
||||
# version bump needs no restart; the service worker (CACHE = "heph-pwa-vN")
|
||||
# evicts stale assets on next load.
|
||||
- name: Ensure heph cache parent directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ heph_pwa_src_dir | dirname }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Stage heph-pwa source at {{ heph_version }}
|
||||
ansible.builtin.git:
|
||||
repo: "{{ heph_repo_url }}"
|
||||
dest: "{{ heph_pwa_src_dir }}"
|
||||
version: "{{ heph_version }}"
|
||||
depth: 1
|
||||
single_branch: true
|
||||
force: true
|
||||
|
||||
- name: Deploy heph LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: heph.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
mode: '0644'
|
||||
notify: Restart heph
|
||||
|
||||
- name: Check if heph LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.heph
|
||||
register: heph_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load heph LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.heph.plist
|
||||
when: heph_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
50
ansible/roles/heph/templates/heph.plist.j2
Normal file
50
ansible/roles/heph/templates/heph.plist.j2
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.heph</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ heph_binary }}</string>
|
||||
<string>--mode</string>
|
||||
<string>server</string>
|
||||
<string>--http-addr</string>
|
||||
<string>{{ heph_http_addr }}</string>
|
||||
<string>--db</string>
|
||||
<string>{{ heph_db }}</string>
|
||||
<string>--socket</string>
|
||||
<string>{{ heph_socket }}</string>
|
||||
<string>--web-root</string>
|
||||
<string>{{ heph_web_root }}</string>
|
||||
<string>--oidc-issuer</string>
|
||||
<string>{{ heph_oidc_issuer }}</string>
|
||||
<string>--oidc-audience</string>
|
||||
<string>{{ heph_oidc_audience }}</string>
|
||||
<string>--self-update</string>
|
||||
<string>--self-update-interval-secs</string>
|
||||
<string>{{ heph_self_update_interval_secs }}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<!-- cargo + toolchain on PATH so --self-update can run `cargo install`. -->
|
||||
<key>PATH</key>
|
||||
<string>{{ heph_bin_dir }}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/erichblume</string>
|
||||
<!-- Pin the rustup channel: the launchagent runs without mise, so a bare
|
||||
cargo shim would otherwise use rustup's (stale) default toolchain. -->
|
||||
<key>RUSTUP_TOOLCHAIN</key>
|
||||
<string>{{ heph_rust_toolchain }}</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ heph_log_dir }}/mcquack.heph.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Loading…
Add table
Add a link
Reference in a new issue