Add hephaestus sync hub to indri (launchagent, PWA, device-code OIDC)
Deploy hephd --mode server on indri as a self-updating LaunchAgent managed by Ansible (ansible/roles/heph, tag heph), making indri the canonical heph hub for the hub-and-spoke task/context system. - Server mode on 0.0.0.0:8787, self-update every 10 minutes (cargo install from the public forge URL; ~/.cargo/bin on the agent PATH). - heph-pwa shell served via --web-root straight from a version-pinned checkout, TLS-terminated at heph.ops.eblu.me through Caddy (new caddy_services entry). - New Authentik device-code (RFC 8628) OIDC app 'heph' (public client) plus a default-device-code-flow bound to the default brand's flow_device_code. - Docs: new services/hephaestus.md service card (incl. Path A seeding runbook and the gilbert spoke caveat), indri.md service list, changelog fragment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f6c926f1f5
commit
d99c962fe1
10 changed files with 374 additions and 0 deletions
|
|
@ -260,5 +260,7 @@
|
||||||
tags: cv
|
tags: cv
|
||||||
- role: docs
|
- role: docs
|
||||||
tags: docs
|
tags: docs
|
||||||
|
- role: heph
|
||||||
|
tags: heph
|
||||||
- role: caddy
|
- role: caddy
|
||||||
tags: caddy
|
tags: caddy
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ caddy_services:
|
||||||
- name: devpi
|
- name: devpi
|
||||||
host: "pypi.{{ caddy_domain }}"
|
host: "pypi.{{ caddy_domain }}"
|
||||||
backend: "http://localhost:3141"
|
backend: "http://localhost:3141"
|
||||||
|
- name: heph
|
||||||
|
host: "heph.{{ caddy_domain }}"
|
||||||
|
backend: "http://localhost:8787" # hephaestus hub (server mode) + PWA shell
|
||||||
- name: kiwix
|
- name: kiwix
|
||||||
host: "kiwix.{{ caddy_domain }}"
|
host: "kiwix.{{ caddy_domain }}"
|
||||||
backend: "https://kiwix.tail8d86e.ts.net"
|
backend: "https://kiwix.tail8d86e.ts.net"
|
||||||
|
|
|
||||||
42
ansible/roles/heph/defaults/main.yml
Normal file
42
ansible/roles/heph/defaults/main.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
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
|
||||||
81
ansible/roles/heph/tasks/main.yml
Normal file
81
ansible/roles/heph/tasks/main.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
---
|
||||||
|
# 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"
|
||||||
|
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
|
||||||
46
ansible/roles/heph/templates/heph.plist.j2
Normal file
46
ansible/roles/heph/templates/heph.plist.j2
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?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>
|
||||||
|
</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>
|
||||||
|
|
@ -434,3 +434,79 @@ data:
|
||||||
provider: !KeyOf mealie-provider
|
provider: !KeyOf mealie-provider
|
||||||
meta_launch_url: https://meals.ops.eblu.me
|
meta_launch_url: https://meals.ops.eblu.me
|
||||||
policy_engine_mode: all
|
policy_engine_mode: all
|
||||||
|
|
||||||
|
heph.yaml: |
|
||||||
|
version: 1
|
||||||
|
metadata:
|
||||||
|
name: BlumeOps Heph SSO
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/description: "Hephaestus hub OIDC (device-code) provider, application, and device-code flow"
|
||||||
|
entries:
|
||||||
|
# Device-code flow (RFC 8628). authentik ships no default for this, so we
|
||||||
|
# create one and bind it to the brand below. An empty stage_configuration
|
||||||
|
# flow is sufficient: the already-authenticated user just confirms the code.
|
||||||
|
- model: authentik_flows.flow
|
||||||
|
id: device-code-flow
|
||||||
|
identifiers:
|
||||||
|
slug: default-device-code-flow
|
||||||
|
attrs:
|
||||||
|
name: Device code flow
|
||||||
|
title: Device code flow
|
||||||
|
slug: default-device-code-flow
|
||||||
|
designation: stage_configuration
|
||||||
|
authentication: require_authenticated
|
||||||
|
|
||||||
|
# Enable the device-code grant globally by binding the flow to the default
|
||||||
|
# brand (domain authentik-default). Partial update — only sets this field.
|
||||||
|
- model: authentik_brands.brand
|
||||||
|
identifiers:
|
||||||
|
domain: authentik-default
|
||||||
|
attrs:
|
||||||
|
flow_device_code: !KeyOf device-code-flow
|
||||||
|
|
||||||
|
# OAuth2 provider for heph — PUBLIC client (device-code + PKCE, no secret).
|
||||||
|
# client_id doubles as the token audience the hub verifies (--oidc-audience heph),
|
||||||
|
# and the app slug 'heph' is the issuer path (/application/o/heph/).
|
||||||
|
- model: authentik_providers_oauth2.oauth2provider
|
||||||
|
id: heph-provider
|
||||||
|
identifiers:
|
||||||
|
name: Heph
|
||||||
|
attrs:
|
||||||
|
name: Heph
|
||||||
|
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||||
|
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||||
|
client_type: public
|
||||||
|
client_id: heph
|
||||||
|
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||||
|
property_mappings:
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
|
||||||
|
sub_mode: hashed_user_id
|
||||||
|
include_claims_in_id_token: true
|
||||||
|
|
||||||
|
# Heph application — linked to the OAuth2 provider
|
||||||
|
- model: authentik_core.application
|
||||||
|
id: heph-app
|
||||||
|
identifiers:
|
||||||
|
slug: heph
|
||||||
|
attrs:
|
||||||
|
name: Hephaestus
|
||||||
|
slug: heph
|
||||||
|
provider: !KeyOf heph-provider
|
||||||
|
meta_launch_url: https://heph.ops.eblu.me
|
||||||
|
policy_engine_mode: any
|
||||||
|
|
||||||
|
# Policy binding — restrict heph to admins group (single-owner, sensitive data)
|
||||||
|
- model: authentik_policies.policybinding
|
||||||
|
identifiers:
|
||||||
|
order: 0
|
||||||
|
target: !KeyOf heph-app
|
||||||
|
group: !Find [authentik_core.group, [name, admins]]
|
||||||
|
attrs:
|
||||||
|
target: !KeyOf heph-app
|
||||||
|
group: !Find [authentik_core.group, [name, admins]]
|
||||||
|
order: 0
|
||||||
|
enabled: true
|
||||||
|
negate: false
|
||||||
|
timeout: 30
|
||||||
|
|
|
||||||
1
docs/changelog.d/heph-indri-hub.infra.md
Normal file
1
docs/changelog.d/heph-indri-hub.infra.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Added the [[hephaestus]] (`heph`) sync hub to indri as a self-updating LaunchAgent managed by Ansible (`ansible/roles/heph`, tag `heph`). The hub runs `hephd --mode server` behind `heph.ops.eblu.me` (Caddy TLS), with self-update on a 10-minute interval and the heph-pwa mobile shell served from `--web-root`. Access is gated by a new Authentik device-code (RFC 8628) OIDC application. Indri is now the canonical hub; other devices (e.g. gilbert) attach as offline-capable spokes. The hub's store was seeded from gilbert via the data-safe Path A bring-up (copy store, reset `meta.origin`).
|
||||||
|
|
@ -33,6 +33,7 @@ Primary BlumeOps server. Mac Mini M1 (2020).
|
||||||
- [[alloy|Alloy]] - Metrics/logs collector
|
- [[alloy|Alloy]] - Metrics/logs collector
|
||||||
- [[caddy]] - Reverse proxy for `*.ops.eblu.me`
|
- [[caddy]] - Reverse proxy for `*.ops.eblu.me`
|
||||||
- [[devpi]] - PyPI mirror (LaunchAgent)
|
- [[devpi]] - PyPI mirror (LaunchAgent)
|
||||||
|
- [[hephaestus]] - heph task/context sync hub (LaunchAgent, self-updating)
|
||||||
- [[cv]] - Static CV site, served by Caddy
|
- [[cv]] - Static CV site, served by Caddy
|
||||||
- [[docs]] - Quartz-built docs site, served by Caddy
|
- [[docs]] - Quartz-built docs site, served by Caddy
|
||||||
|
|
||||||
|
|
|
||||||
116
docs/reference/services/hephaestus.md
Normal file
116
docs/reference/services/hephaestus.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
---
|
||||||
|
title: Hephaestus
|
||||||
|
modified: 2026-06-04
|
||||||
|
last-reviewed: 2026-06-04
|
||||||
|
tags:
|
||||||
|
- service
|
||||||
|
- hephaestus
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hephaestus
|
||||||
|
|
||||||
|
[hephaestus](https://github.com/eblume/hephaestus) (`heph`) is the user's
|
||||||
|
self-hosted task + context/knowledge system. It is **hub-and-spoke**: each device
|
||||||
|
runs a full local SQLite replica (`hephd --mode local`) and background-syncs
|
||||||
|
against one canonical **hub**. Indri runs that hub.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **URL** | https://heph.ops.eblu.me (PWA + sync endpoint) |
|
||||||
|
| **Local Port** | 8787 (`hephd --mode server`, bound `0.0.0.0`) |
|
||||||
|
| **Binary** | `~/.cargo/bin/hephd` (self-updating) |
|
||||||
|
| **Data** | `~/.local/share/heph/heph.db` |
|
||||||
|
| **PWA shell** | `~/.local/share/heph/web` |
|
||||||
|
| **Logs** | `~/Library/Logs/mcquack.heph.{out,err}.log` |
|
||||||
|
| **LaunchAgent** | `mcquack.eblume.heph` |
|
||||||
|
| **Ansible role** | `ansible/roles/heph` (tag `heph`) |
|
||||||
|
|
||||||
|
## What runs on indri
|
||||||
|
|
||||||
|
The launchagent runs the hub in server mode with three features enabled:
|
||||||
|
|
||||||
|
```
|
||||||
|
hephd --mode server --http-addr 0.0.0.0:8787 --db ~/.local/share/heph/heph.db
|
||||||
|
--web-root ~/.local/share/heph/web
|
||||||
|
--oidc-issuer https://authentik.ops.eblu.me/application/o/heph/
|
||||||
|
--oidc-audience heph
|
||||||
|
--self-update --self-update-interval-secs 600
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Server mode** exposes the HTTP sync endpoint (`/rpc`, `/sync/*`) that spokes
|
||||||
|
reconcile their op-log against.
|
||||||
|
- **Self-update** (10-minute poll) rebuilds `hephd` from the forge when a newer
|
||||||
|
release tag appears (`cargo install --git https://forge.eblu.me/eblume/hephaestus.git`).
|
||||||
|
Indri's Rust toolchain (`~/.cargo/bin`) is on the agent's `PATH` for this.
|
||||||
|
- **PWA** (`--web-root`) serves the [heph-pwa] mobile shell; Caddy terminates TLS
|
||||||
|
at `heph.ops.eblu.me` so the PWA runs in a secure context (service worker,
|
||||||
|
install-to-home-screen, voice capture).
|
||||||
|
|
||||||
|
[heph-pwa]: https://github.com/eblume/hephaestus
|
||||||
|
|
||||||
|
The hub binds `0.0.0.0` so tailnet spokes can also sync directly
|
||||||
|
(`http://indri.tail8d86e.ts.net:8787`); access is gated by Authentik OIDC either
|
||||||
|
way — tailnet reachability alone is not enough.
|
||||||
|
|
||||||
|
## Authentication (Authentik OIDC, device-code)
|
||||||
|
|
||||||
|
The hub verifies an OIDC bearer token on every sync. The `heph` application is a
|
||||||
|
**public** OAuth2 client using the **device-code flow** (RFC 8628), provisioned
|
||||||
|
in the [[authentik]] blueprint (`argocd/manifests/authentik/configmap-blueprint.yaml`):
|
||||||
|
|
||||||
|
- Issuer: `https://authentik.ops.eblu.me/application/o/heph/`
|
||||||
|
- Audience / client id: `heph`
|
||||||
|
- Restricted to the `admins` group (single-owner, sensitive data).
|
||||||
|
|
||||||
|
Because no Authentik instance ships a device-code flow by default, the blueprint
|
||||||
|
also creates `default-device-code-flow` and binds it to the default brand's
|
||||||
|
`flow_device_code`. Devices obtain a token with `heph auth login`; the PWA
|
||||||
|
currently takes a pasted token (in-app device-code login is upstream follow-up).
|
||||||
|
|
||||||
|
## Data seeding (Path A, one-time)
|
||||||
|
|
||||||
|
The hub was seeded from the existing `gilbert` device so no task history was
|
||||||
|
lost. heph's data-safe bring-up ("Path A") has the hub **adopt the device's
|
||||||
|
identity** rather than rewriting the device:
|
||||||
|
|
||||||
|
1. Quiesce the seed device: `heph daemon stop` (on gilbert).
|
||||||
|
2. Copy its store to indri: `scp ~/.local/share/heph/heph.db indri:~/.local/share/heph/heph.db`.
|
||||||
|
3. 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):
|
||||||
|
```fish
|
||||||
|
ssh indri "sqlite3 ~/.local/share/heph/heph.db \"DELETE FROM meta WHERE key='origin';\""
|
||||||
|
```
|
||||||
|
4. `mise run provision-indri -- --tags heph` (installs hephd, stages the PWA,
|
||||||
|
loads the launchagent → hub starts on the seeded store).
|
||||||
|
|
||||||
|
Only `meta.origin` changes; `owner_id`, nodes, op-log, and links are copied
|
||||||
|
untouched. A clean `hephd --owner-id` / seed command is tracked upstream as
|
||||||
|
hephaestus follow-up — until then this manual reset is the documented path.
|
||||||
|
|
||||||
|
## Connecting a spoke (e.g. gilbert)
|
||||||
|
|
||||||
|
A device joins by running its local daemon with the hub URL + OIDC client and
|
||||||
|
logging in once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hephd --mode local --hub-url https://heph.ops.eblu.me \
|
||||||
|
--oidc-issuer https://authentik.ops.eblu.me/application/o/heph/ \
|
||||||
|
--oidc-client-id heph
|
||||||
|
heph auth login --hub-url https://heph.ops.eblu.me \
|
||||||
|
--issuer https://authentik.ops.eblu.me/application/o/heph/ --client-id heph
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Caveat:** `heph daemon` cannot yet bake hub/spoke flags into the generated
|
||||||
|
> launchd plist (upstream gap). On a spoke whose plist is managed by `heph
|
||||||
|
> daemon`, the hub/OIDC flags must be hand-added — and a later `heph daemon
|
||||||
|
> start/restart` will regenerate the plist and drop them. Avoid `heph daemon`
|
||||||
|
> subcommands on a configured spoke until that gap is closed; reload via
|
||||||
|
> `launchctl` instead.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [[indri]] — host
|
||||||
|
- [[authentik]] — OIDC provider
|
||||||
|
- [[caddy]] — TLS termination for `heph.ops.eblu.me`
|
||||||
Loading…
Add table
Add a link
Reference in a new issue