Compare commits

..

6 Commits

Author SHA1 Message Date
39080333da Complete ops-hub Inter-Hub bootstrap and close bootstrap workplans.
Extract bootstrap logic into src/ops_hub/bootstrap.py with dry-run support,
public hub discovery, and unit tests. Update the gate probe to accept public
hub listing (200 or 401), document dev commands and architecture, and archive
finished OPS-WP-0001 and OPS-WP-0002 workplans.
2026-07-07 01:49:12 +02:00
custodian-sync
0454e126cb chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-07:
  - update .custodian-brief.md for ops-hub
2026-07-07 01:41:14 +02:00
custodian-sync
ad7f148c36 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-07:
  - update .custodian-brief.md for ops-hub
2026-07-07 01:35:02 +02:00
c68553673d Add registry/NO_CAPABILITIES.md (reuse-surface REUSE-WP-0017-T03)
Reviewed for the reuse-surface capability registry coverage campaign;
no reusable capability to register at this time. See file for rationale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 18:50:37 +02:00
057ce6d888 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-03:
  - update .custodian-brief.md for ops-hub
2026-07-03 18:53:16 +02:00
21bfd5fa49 Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:27 +02:00
28 changed files with 1639 additions and 405 deletions

20
.claude/rules/agents.md Normal file
View File

@@ -0,0 +1,20 @@
## Kaizen Agents
Specialized agent personas available on demand via the state-hub MCP.
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
Common agents:
| Agent | Category | When to use |
|-------|----------|-------------|
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
| `test-maintenance` | testing | Diagnose and fix failing tests |
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
| `keepaTodofile` | process | Maintain TODO.md during work |
| `project-management` | process | Track status, determine next steps |
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
All 17 agents: call `list_kaizen_agents()` for the full list.

View File

@@ -0,0 +1,29 @@
## Architecture
`ops-hub` is a Python tooling repo that extends Inter-Hub with VSM Operations
(System 1) vocabulary, bootstrap clients, and gate probes. Inter-Hub remains the
generic hub substrate; this repo owns domain-specific seeds, scripts, and library
code.
### Components
| Path | Role |
| --- | --- |
| `src/ops_hub/interhub_gate_probe.py` | Public gate probe for bootstrap API surface |
| `src/ops_hub/bootstrap.py` | Authenticated bootstrap client (plan, dry-run, execute) |
| `scripts/interhub-gate-probe.py` | CLI wrapper for gate probe |
| `scripts/ops-hub-bootstrap-api.py` | CLI wrapper for bootstrap |
| `seeds/` | Manifest, widget, and SQL bootstrap artifacts |
| `docs/` | Runbook, readiness gates, inventory handoff |
| `workplans/` | ADR-001 work source of truth |
### External integrations
- **Inter-Hub** (`https://hub.coulomb.social`) — hub rows, manifests, widgets, events
- **State Hub** — workstream/task cache rebuilt from workplan files
- **OpenBao** — operator and runtime API key custody (`warden access --exec`)
- **ops-warden** — SSH cert issuance and credential routing only
## Quick Reference
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference

View File

@@ -0,0 +1,50 @@
# Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-hub` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`

View File

@@ -0,0 +1,38 @@
## First Session Protocol
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/OPS-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
create_workstream(topic_id="1f2e4d10-c967-4803-ae6c-7f4b4e806409", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
```
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured infotech into N workstreams, M tasks",
event_type="milestone",
topic_id="1f2e4d10-c967-4803-ae6c-7f4b4e806409",
detail={"workstreams": [...], "tasks_created": M}
)
```
<!-- Delete or archive this file once past first session -->

View File

@@ -0,0 +1,11 @@
## Repo boundary
This repo owns **ops-hub** only. It does not own:
- Generic Inter-Hub framework, API substrate, auth → `inter-hub/`
- State Hub workstream/task/decision implementation → `state-hub/`
- SSH CA, credential routing catalog, bootstrap SSH envelope → `ops-warden/`
- OpenBao mounts, platform secret paths, ESO → `railiance-platform/`
- Cluster desired state, host principals, force-command wrappers → `railiance-infra/`
- HelixForge architecture handoff canon → `helix-forge/` (linked, not duplicated)
- Identity/OIDC/MFA → `key-cape/` / Keycloak

View File

@@ -0,0 +1,5 @@
**Purpose:** Operations / System 1 extension for Inter-Hub, focused on operational truth, readiness evidence, service catalog records, and migration gates.
**Domain:** infotech
**Repo slug:** ops-hub
**Topic ID:** 1f2e4d10-c967-4803-ae6c-7f4b4e806409

View File

@@ -0,0 +1,85 @@
## Session Protocol
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
Read the offline-safe brief first — it works without a live hub connection:
```bash
cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("infotech")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
```
If the hub is offline: `cd ~/state-hub && make api`
**Step 2 — Check inbox**
With MCP tools:
```
get_messages(to_agent="ops-hub", unread_only=True)
```
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
requests before proceeding.
Without MCP tools:
```bash
curl -s "http://127.0.0.1:8000/messages/?to_agent=ops-hub&unread_only=true" \
| python3 -m json.tool
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
**Step 3 — Scan workplans**
```bash
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:ops-hub]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
- `alignment_warnings`: flag if active work is not aligned with current goal
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="1f2e4d10-c967-4803-ae6c-7f4b4e806409", workstream_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"1f2e4d10-c967-4803-ae6c-7f4b4e806409","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
```bash
git -C <repo_path> pull --ff-only
cd ~/state-hub && make fix-consistency REPO=ops-hub
```
For repos where implementation runs on a remote machine (e.g. CoulombCore),
use the combined target which pulls before fixing:
```bash
cd ~/state-hub && make fix-consistency-remote REPO=ops-hub
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
until you pull — intentional to prevent clobbering remote progress.

View File

@@ -0,0 +1,27 @@
## Stack
- **Language:** Python 3.11+
- **Key deps:** stdlib only (`pyproject.toml` has no runtime dependencies)
- **Layout:** `src/ops_hub/` package, `scripts/` operator CLIs, `tests/` unittest suite
## Dev Commands
```bash
# Show Makefile targets
make help
# Unit tests
make test
# Probe production Inter-Hub bootstrap API gate (no secrets)
make interhub-gate
# Plan attended bootstrap (requires IHUB_OPERATOR_KEY_FILE)
make interhub-bootstrap-dry-run IHUB_OPERATOR_KEY_FILE=/path/to/key
# Attended production bootstrap
make interhub-bootstrap IHUB_OPERATOR_KEY_FILE=/path/to/key
# Sync workplan files to State Hub after edits
cd ~/state-hub && make fix-consistency REPO=ops-hub
```

View File

@@ -0,0 +1,40 @@
## Workplan Convention (ADR-001)
File location: `workplans/OPS-WP-NNNN-<slug>.md`
ID prefix: `OPS-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-OPS-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:ops-hub]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
Task blocks use this shape:
```task
id: OPS-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -1,8 +1,8 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually --> <!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — ops-hub # Custodian Brief — ops-hub
**Domain:** (unknown) **Domain:** infotech
**Last synced:** 2026-06-06 17:21 UTC **Last synced:** 2026-07-06 23:41 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams ## Active Workstreams
@@ -13,6 +13,6 @@
## MCP Orientation (when available) ## MCP Orientation (when available)
If the state-hub MCP server is reachable, call: If the state-hub MCP server is reachable, call:
`get_domain_summary("")` `get_domain_summary("infotech")`
This provides richer cross-domain context. This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source. If the MCP call fails, use this file as your orientation source.

View File

@@ -2,9 +2,9 @@
## Repo Identity ## Repo Identity
**Purpose:** Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective. **Purpose:** Operations / System 1 extension for Inter-Hub, focused on operational truth, readiness evidence, service catalog records, and migration gates.
**Domain:** inter_hub **Domain:** infotech
**Repo slug:** ops-hub **Repo slug:** ops-hub
**Topic ID:** `1f2e4d10-c967-4803-ae6c-7f4b4e806409` **Topic ID:** `1f2e4d10-c967-4803-ae6c-7f4b4e806409`
**Workplan prefix:** `OPS-WP-` **Workplan prefix:** `OPS-WP-`
@@ -101,6 +101,81 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
--- ---
## Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-hub` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
## Dev Commands
```bash
make help # list targets
make test # unit tests (PYTHONPATH=src)
make interhub-gate # public Inter-Hub bootstrap gate probe (no secrets)
```
Attended bootstrap (requires `IHUB_OPERATOR_KEY` via file or `warden access --exec`):
```bash
make interhub-bootstrap-dry-run IHUB_OPERATOR_KEY_FILE=/path/to/key
make interhub-bootstrap IHUB_OPERATOR_KEY_FILE=/path/to/key
```
Credential routing for operator/runtime keys: `warden route find "Inter-Hub operator key"`.
Runbook: `docs/bootstrap-runbook.md`.
---
## Workplan Convention (ADR-001) ## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a Work items originate as files in this repo — not in the hub. The hub is a
@@ -124,7 +199,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
id: OPS-WP-NNNN id: OPS-WP-NNNN
type: workplan type: workplan
title: "..." title: "..."
domain: inter_hub domain: infotech
repo: ops-hub repo: ops-hub
status: proposed | ready | active | blocked | backlog | finished | archived status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex owner: codex

12
CLAUDE.md Normal file
View File

@@ -0,0 +1,12 @@
# ops-hub — Claude Code Instructions
@SCOPE.md
@.claude/rules/repo-identity.md
@.claude/rules/session-protocol.md
@.claude/rules/first-session.md
@.claude/rules/workplan-convention.md
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

View File

@@ -7,9 +7,16 @@ updated: "2026-06-06"
## Why it exists ## Why it exists
Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective. `ops-hub` is the Operations / System 1 extension for Inter-Hub. It turns
operational reality into governed, queryable, and evidence-backed hub records:
environments, hosts, clusters, services, endpoints, releases, backups,
incidents, risks, runbooks, readiness gates, and migration waves.
Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective. It exists because Railiance and HelixForge operations need a durable
operational truth surface while the current CoulombCore environment transitions
toward the ThreePhoenix production shape. State Hub continues to own
workstreams and decisions; Inter-Hub continues to own the generic hub substrate.
`ops-hub` owns the operations extension behavior built on top of that substrate.
## Governing principle ## Governing principle
@@ -17,8 +24,16 @@ This repository should stay focused on the purpose above. Work that changes its
authority, ownership boundaries, or operational promises should be captured in a authority, ownership boundaries, or operational promises should be captured in a
workplan before implementation. workplan before implementation.
The first implementation rule is: domain-specific runtime code belongs here,
while generic hub framework behavior belongs in `inter-hub`.
## What it enables ## What it enables
- A coding agent can understand why the repository exists before changing it. - Operators can see what runs where, how it is reached, and what evidence proves
- State Hub can register and coordinate work for this repository. it is healthy.
- Future workplans can stay connected to the repository's intended role. - Collectors, adapters, and scheduled probes can report operational facts into
Inter-Hub using the ops vocabulary.
- Readiness and migration gates can be represented as explicit, auditable
operational records.
- Future VSM hubs can reuse the extension pattern without turning Inter-Hub
itself into a domain-specific operations product.

View File

@@ -2,13 +2,15 @@ IHUB_BASE ?= https://hub.coulomb.social
IHUB_OPERATOR_KEY_FILE ?= IHUB_OPERATOR_KEY_FILE ?=
OPS_HUB_RUNTIME_KEY_OUTPUT ?= OPS_HUB_RUNTIME_KEY_OUTPUT ?=
.PHONY: help interhub-gate interhub-bootstrap interhub-bootstrap-help .PHONY: help interhub-gate interhub-bootstrap interhub-bootstrap-dry-run interhub-bootstrap-help test
help: help:
@echo "Targets:" @echo "Targets:"
@echo " interhub-gate Probe whether production Inter-Hub exposes the ops-hub bootstrap API surface" @echo " interhub-gate Probe whether production Inter-Hub exposes the ops-hub bootstrap API surface"
@echo " interhub-bootstrap-help Show bootstrap helper options" @echo " interhub-bootstrap-help Show bootstrap helper options"
@echo " interhub-bootstrap-dry-run Plan bootstrap actions with IHUB_OPERATOR_KEY_FILE (no mutations)"
@echo " interhub-bootstrap Run attended ops-hub Inter-Hub bootstrap with IHUB_OPERATOR_KEY_FILE" @echo " interhub-bootstrap Run attended ops-hub Inter-Hub bootstrap with IHUB_OPERATOR_KEY_FILE"
@echo " test Run unit tests"
interhub-gate: interhub-gate:
IHUB_BASE="$(IHUB_BASE)" python3 scripts/interhub-gate-probe.py IHUB_BASE="$(IHUB_BASE)" python3 scripts/interhub-gate-probe.py
@@ -16,6 +18,13 @@ interhub-gate:
interhub-bootstrap-help: interhub-bootstrap-help:
python3 scripts/ops-hub-bootstrap-api.py --help python3 scripts/ops-hub-bootstrap-api.py --help
interhub-bootstrap-dry-run:
@test -n "$(IHUB_OPERATOR_KEY_FILE)" || \
(echo "IHUB_OPERATOR_KEY_FILE is required; materialize the operator key into a 0600 temp file first." >&2; exit 2)
IHUB_BASE="$(IHUB_BASE)" \
IHUB_OPERATOR_KEY_FILE="$(IHUB_OPERATOR_KEY_FILE)" \
python3 scripts/ops-hub-bootstrap-api.py --dry-run
interhub-bootstrap: interhub-bootstrap:
@test -n "$(IHUB_OPERATOR_KEY_FILE)" || \ @test -n "$(IHUB_OPERATOR_KEY_FILE)" || \
(echo "IHUB_OPERATOR_KEY_FILE is required; materialize the operator key into a 0600 temp file first." >&2; exit 2) (echo "IHUB_OPERATOR_KEY_FILE is required; materialize the operator key into a 0600 temp file first." >&2; exit 2)
@@ -23,3 +32,6 @@ interhub-bootstrap:
IHUB_OPERATOR_KEY_FILE="$(IHUB_OPERATOR_KEY_FILE)" \ IHUB_OPERATOR_KEY_FILE="$(IHUB_OPERATOR_KEY_FILE)" \
OPS_HUB_RUNTIME_KEY_OUTPUT="$(OPS_HUB_RUNTIME_KEY_OUTPUT)" \ OPS_HUB_RUNTIME_KEY_OUTPUT="$(OPS_HUB_RUNTIME_KEY_OUTPUT)" \
python3 scripts/ops-hub-bootstrap-api.py python3 scripts/ops-hub-bootstrap-api.py
test:
PYTHONPATH=src python3 -m unittest discover -s tests -v

View File

@@ -1 +1,6 @@
Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective. Operations / System 1 extension for Inter-Hub.
`ops-hub` is the operational truth surface for environments, hosts, clusters,
services, endpoints, releases, backups, incidents, risks, runbooks, readiness
gates, and migration waves. Generic hub framework work stays in `inter-hub`;
operations-specific extension code belongs here.

View File

@@ -1,32 +1,52 @@
# SCOPE # SCOPE
> This file was generated by `statehub register`. Refine it as the repository
> boundaries become clearer.
## One-liner ## One-liner
Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective. Operations / System 1 extension for Inter-Hub, focused on operational truth,
readiness evidence, and migration gates.
## Core Idea ## Core Idea
ops-hub exists to provide the capability described in INTENT.md. `ops-hub` is a domain-specific Inter-Hub extension. It should professionalize
operations by making environments, hosts, clusters, services, endpoints,
releases, backups, incidents, risks, runbooks, readiness gates, and migration
waves explicit and evidence-backed.
The repo is intentionally separate from `inter-hub`: generic framework and API
substrate work remains in `inter-hub`; operations-specific collectors,
adapters, probes, bootstrap clients, UI/extensions, tests, and packaging belong
here.
## In Scope ## In Scope
- Maintain the repository's primary implementation. - Operations hub implementation code and tests.
- Keep docs, tests, and operational metadata current. - Ops vocabulary clients, collectors, adapters, and scheduled probes.
- Inter-Hub bootstrap/smoke tooling for the `ops-hub` extension.
- Operations service catalog, readiness, migration, endpoint, backup, restore,
incident, and runbook models.
- Repo-local workplans for growing the Operations / System 1 extension.
## Out of Scope ## Out of Scope
- Own unrelated adjacent systems. - Generic Inter-Hub framework behavior, API substrate, authentication, or
- Make irreversible operational decisions without human approval. registry semantics.
- State Hub workstream, task, decision, or progress implementation.
- Railiance infrastructure, cluster, platform, enablement, or app desired state.
- Manual production DB seeding unless the operator explicitly chooses that
fallback.
- Irreversible operational decisions without human approval.
## Current State ## Current State
- Status: active; implementation and stability should be verified by the repo agent. - Status: production bootstrap complete (2026-07-07).
- Implementation: Python package under `src/ops_hub/`, operator scripts, seeds,
tests, and Makefile targets for gate probe and attended bootstrap.
- Inter-Hub: `ops-hub` hub, active manifest, seed widgets, and first
`ops-endpoint-verified` event are live in production.
## Getting Oriented ## Getting Oriented
- Start with: INTENT.md - Start with: INTENT.md
- Agent instructions: AGENTS.md - Agent instructions: AGENTS.md
- Workplans: workplans/ - Workplans: workplans/
- HelixForge handoff: `/home/worsch/helix-forge/workplans/HF-WP-0001-establish-ops-hub-first-extension.md`

15
docs/README.md Normal file
View File

@@ -0,0 +1,15 @@
# ops-hub Docs
This directory contains the first repo-local version of the HelixForge
`HF-WP-0001` handoff.
- `initial-inventory.md` defines the first environment, host, cluster, service,
and endpoint catalog.
- `readiness-gates.md` defines the CoulombCore-to-ThreePhoenix readiness model.
- `bootstrap-runbook.md` defines the operator-ready Inter-Hub bootstrap path.
- `../seeds/ops-hub-manifest.draft.json` contains the initial capability
manifest draft.
- `../seeds/ops-hub-widgets.seed.json` contains the initial widget seed.
- `../seeds/ops-hub-bootstrap.sql` is an operator-approved fallback only; do
not use direct DB seeding while the supported Inter-Hub API path is viable or
pending.

View File

@@ -47,6 +47,14 @@ operator_key_file="$(mktemp)"
# source. Do not echo it into shared logs. # source. Do not echo it into shared logs.
``` ```
Plan the bootstrap without mutations:
```bash
make interhub-bootstrap-dry-run \
IHUB_BASE=https://hub.coulomb.social \
IHUB_OPERATOR_KEY_FILE="$operator_key_file"
```
Run the attended bootstrap: Run the attended bootstrap:
```bash ```bash
@@ -161,11 +169,12 @@ the authenticated UI.
## Current Live-Execution Blocker ## Current Live-Execution Blocker
The repo-side helper and runbook are ready. The remaining blocker is an The repo-side helper, dry-run path, and runbook are ready. The remaining step is
authenticated production execution lane: an operator-attended production run with:
- an operator-provided `IHUB_OPERATOR_KEY_FILE`, - an operator-provided `IHUB_OPERATOR_KEY_FILE`,
- an OpenBao-materialized key on a trusted host, or - an OpenBao-materialized key on a trusted host, or
- an explicitly approved deployment-side migration/bootstrap path. - an explicitly approved deployment-side migration/bootstrap path.
Until one of those is available, run only local validation and gate probes. Until one of those is available, run `make interhub-gate`, `make test`, and
`make interhub-bootstrap-dry-run` for local validation.

146
docs/initial-inventory.md Normal file
View File

@@ -0,0 +1,146 @@
# Ops Hub Initial Inventory
Date: 2026-06-06
## Purpose
This document is the first structured inventory for `ops-hub`, the VSM
Operations / System 1 hub. It turns the current operations situation into a
catalogable model for this implementation repo.
Source background:
- `/home/worsch/helix-forge/wiki/CurrentOperationsSituation.md`
- `/home/worsch/helix-forge/workplans/HF-WP-0001-establish-ops-hub-first-extension.md`
## Repository Boundary
As of 2026-06-06, `ops-hub` implementation belongs in `/home/worsch/ops-hub`
with remote `gitea-remote:coulomb/ops-hub.git`.
- `ops-hub` owns collectors, adapters, scheduled probes, runtime
packaging, UI/extensions, tests, and Inter-Hub bootstrap/smoke clients.
- `inter-hub` remains the generic hub framework, manifest/registry substrate,
authentication surface, widget/event API, and bootstrap API owner.
- `helix-forge` keeps architecture context and the original coordinating
workplan.
- Railiance repos own deployable infrastructure/service state and the
operational evidence that `ops-hub` should surface.
## VSM Placement
| Field | Value |
|---|---|
| Hub | `ops-hub` |
| Hub family | `vsm` |
| VSM function | `OPS` |
| VSM system | `S1` |
| Primary concern | Operational truth and evidence |
`ops-hub` owns the description of what is currently running, where it runs, how
it is reached, what state it is in, and what operational evidence exists. It
does not replace State Hub workstreams or Inter-Hub governance.
## Environments
| Environment | Role | Current state | Notes |
|---|---|---|---|
| `local` | Workstation development and local services | Active, important, not production | Hosts State Hub and local build/runtime pieces. |
| `coulombcore` | Live transitional production | Active, production-like, historically hand-built | Public IP `92.205.130.254`; runs current Gitea and experimental operational services. |
| `railiance01` | Future production foundation | Provisioning target | Public IP `92.205.62.239`; first server of intended ThreePhoenix shape. |
| `threephoenix-prod` | Target production topology | Planned | Future governed multi-node production environment. |
## Hosts
| Host | Environment | Address | Role | Known gaps |
|---|---|---|---|---|
| `coulombcore` | `coulombcore` | `92.205.130.254` | Current live production-like server | Needs service catalog, drift tracking, backup/restore evidence, and migration disposition. |
| `railiance01` | `railiance01` | `92.205.62.239` | First ThreePhoenix production foundation node | Needs full inventory, readiness gates, and cluster/platform bootstrap evidence. |
| local workstation | `local` | local/private | State Hub and development runtime host | Needs explicit service ownership and backup expectations. |
Ops Bridge may provide reachability evidence for connected servers, but it is
not the service catalog. `ops-hub` should turn bridge reachability into
inventory signals rather than treating the bridge itself as the inventory.
## Clusters
| Cluster | Environment | Role | Current notes |
|---|---|---|---|
| CoulombCore Kubernetes | `coulombcore` | Current operational Kubernetes runtime | Hosts current Gitea deployment and related services. |
| ThreePhoenix Kubernetes | `threephoenix-prod` | Target production runtime | Future governed production cluster assembled through Railiance repos. |
## Services
| Service | Current environment | Owner repo | Current evidence | Gaps |
|---|---|---|---|---|
| Gitea | `coulombcore` | `railiance-apps` | Helm release `gitea`, namespace `default`, app version `1.25.4`, NodePort `32166`, public registry path returns auth challenge. | SOPS Helm values update, package token, `docker login`, push, pull, backup coverage, restore evidence. |
| Gitea database | `coulombcore` | `railiance-platform` | Database `gitea-db` in namespace `databases`. | Backup and restore evidence not recorded here yet. |
| Gitea shared storage | `coulombcore` | `railiance-platform` / `railiance-apps` | PVC `default/gitea-shared-storage`. | Package blob backup and restore evidence not confirmed. |
| State Hub | `local` | `the-custodian/state-hub` | Local API and dashboard are operational enough for repo registration and workplan sync. | Future cluster deployment/readiness still needs gates and evidence. |
| Inter-Hub | live public endpoint | `inter-hub` | `https://hub.coulomb.social/api/v2/openapi.json` and docs are reachable. | Hub bootstrap still depends on authenticated UI or migration. |
| Ops Bridge | local/remote bridge | `ops-bridge` | Useful for connected-server visibility. | Not a service catalog; should emit reachability evidence into `ops-hub`. |
## Endpoints
| Endpoint | Service | Environment | Current status | Evidence |
|---|---|---|---|---|
| `https://gitea.coulomb.social/v2/` | Gitea OCI registry | `coulombcore` | Route fixed; returns registry auth challenge | Expected `401` with OCI registry challenge. |
| `https://hub.coulomb.social/api/v2/openapi.json` | Inter-Hub API | live Inter-Hub | Reachable | OpenAPI document fetched on 2026-05-16. |
| `https://hub.coulomb.social/Hubs` | Inter-Hub UI | live Inter-Hub | Requires login | Redirects to `/NewSession`. |
| `http://127.0.0.1:8000/state/health` | State Hub API | `local` | Reachable locally | Used for StateHub registration/sync. |
## Service Catalog Gap
There is no central place that answers these questions:
- What runs where?
- Which repo owns its desired state?
- Which endpoint exposes it?
- Which data stores back it?
- Which backups and restore tests cover it?
- Which migration wave will replace or move it?
- Which current evidence proves it is healthy?
`ops-hub` should be the first place where these answers are explicit and
machine-addressable.
## First Ops Widgets
Seed these in Inter-Hub once `ops-hub` exists:
- `ops-env-local`
- `ops-env-coulombcore`
- `ops-env-railiance01`
- `ops-env-threephoenix-prod`
- `ops-host-coulombcore`
- `ops-host-railiance01`
- `ops-service-catalog`
- `ops-service-gitea`
- `ops-service-state-hub`
- `ops-service-inter-hub`
- `ops-endpoint-gitea-registry`
- `ops-readiness-gitea-registry`
- `ops-readiness-state-hub-cluster-deploy`
- `ops-migration-coulombcore-to-threephoenix`
## First Evidence Events
The first event should be the Gitea registry endpoint verification:
```json
{
"widgetId": "<ops-endpoint-gitea-registry-widget-id>",
"eventType": "ops-endpoint-verified",
"viewContext": "railiance-apps/workplans/RAIL-AP-WP-0001",
"metadata": {
"vsmFunction": "OPS",
"vsmSystem": "S1",
"endpoint": "https://gitea.coulomb.social/v2/",
"expectedStatus": 401,
"observedHeader": "Docker-Distribution-Api-Version: registry/2.0"
}
}
```
This event is blocked until the ops event type is registered by an active
manifest and the target widget exists.

63
docs/readiness-gates.md Normal file
View File

@@ -0,0 +1,63 @@
# Ops Hub Readiness Gates
Date: 2026-06-06
## Purpose
These gates define what must be true before operational responsibility can move
from the current CoulombCore setup to the future ThreePhoenix production setup.
They are the first repo-local `ops-hub` readiness model.
Statuses:
- `unknown` means no reliable evidence has been cataloged yet.
- `partial` means some evidence exists, but the gate is not complete.
- `blocked` means a required precondition is missing.
- `ready` means the evidence requirement is satisfied.
## Gates
| ID | Gate | Owner repo | Evidence requirement | Current status |
|---|---|---|---|---|
| OPS-G01 | Environment inventory exists | `ops-hub` | `local`, `coulombcore`, `railiance01`, and `threephoenix-prod` are represented with role, lifecycle state, and owner notes. | `partial` |
| OPS-G02 | Service catalog exists | `ops-hub` | Each live and target service has environment, owner repo, endpoint, backing stores, lifecycle state, and evidence links. | `partial` |
| OPS-G03 | DNS and TLS are codified | `railiance-cluster` / `railiance-apps` | Public hostnames, ingress routes, certificate sources, and renewal paths are declared in repo files. | `unknown` |
| OPS-G04 | Git hosting is reproducible | `railiance-apps` / `railiance-platform` | Gitea or successor deployment can be recreated from repo state, including database and storage dependencies. | `partial` |
| OPS-G05 | Container registry publishing is proven | `railiance-apps` | `docker login`, push, and pull succeed against `https://gitea.coulomb.social/v2/` using governed secrets. | `partial` |
| OPS-G06 | Persistent data is backed up | `railiance-platform` | Each persistent data store has backup location, schedule, retention, ownership, and latest successful backup evidence. | `unknown` |
| OPS-G07 | Restore path is proven | `railiance-platform` / `railiance-apps` | Restore test evidence exists for Gitea database, package blobs, and State Hub data. | `unknown` |
| OPS-G08 | Secrets path is governed | `railiance-infra` / `railiance-apps` | SOPS/age keys and operator secret paths are documented; no required secret depends on shell memory. | `partial` |
| OPS-G09 | Cluster runtime is reproducible | `railiance-cluster` | Kubernetes runtime, ingress, CNI, operators, and routing primitives are recreated through repo-owned automation. | `unknown` |
| OPS-G10 | Platform services are reproducible | `railiance-platform` | PostgreSQL/CNPG, object storage, secret management, and identity dependencies have repo-owned deployment evidence. | `unknown` |
| OPS-G11 | Application deployment is reproducible | `railiance-apps` | Gitea, Inter-Hub, State Hub, and other application releases are declared with Helm values and deployment runbooks. | `partial` |
| OPS-G12 | Rollback path is documented | owning service repos | Each migration wave has rollback conditions, steps, and data safety notes. | `unknown` |
| OPS-G13 | Operator runbooks exist | owning service repos | Deploy, restore, rotate, incident response, and migration runbooks exist for each critical service. | `unknown` |
| OPS-G14 | Observability and health checks are explicit | `railiance-cluster` / `railiance-platform` / service repos | Health checks, logs, metrics, and endpoint probes are documented and tied to service catalog entries. | `unknown` |
| OPS-G15 | Inter-Hub ops bootstrap is available | `inter-hub` / `ops-hub` / `helix-forge` | `ops-hub` can be created through UI, supported API, or explicit migration fallback, manifest activated, API consumer/key created, widgets seeded, and events accepted. | `partial` |
## Initial Migration Waves
| Wave | Goal | Required gates |
|---|---|---|
| `wave-0-catalog` | Establish the operational truth surface without moving services. | OPS-G01, OPS-G02, OPS-G15 |
| `wave-1-registry-proof` | Prove current Gitea registry publishing and evidence capture. | OPS-G03, OPS-G05, OPS-G08, OPS-G14 |
| `wave-2-backup-restore` | Confirm backups and restore paths for critical persistent state. | OPS-G06, OPS-G07, OPS-G13 |
| `wave-3-threephoenix-foundation` | Recreate cluster and platform foundations on railiance01/ThreePhoenix. | OPS-G09, OPS-G10 |
| `wave-4-service-migration` | Move or replace production responsibilities from CoulombCore to ThreePhoenix. | OPS-G04, OPS-G11, OPS-G12 plus service-specific gates |
## Evidence Shape
Each readiness gate should eventually be represented in `ops-hub` as a widget
or widget family with events like:
- `ops-readiness-gate-updated`
- `ops-endpoint-verified`
- `ops-backup-verified`
- `ops-restore-tested`
- `ops-risk-raised`
- `ops-migration-gate-passed`
- `ops-migration-gate-failed`
Until Inter-Hub can create all required records through API calls, the evidence
can be maintained in this repo and mirrored into Inter-Hub through the UI or
explicit operator-approved migrations.

View File

@@ -0,0 +1,15 @@
---
repo: ops-hub
reason: >
Only a single diagnostic bootstrap-API probe script exists; the described operational-truth surface (hosts/services/incidents/runbooks) isn't implemented yet.
reviewed: "2026-07-06"
reviewed_by: claude-code
revisit: >
Revisit once the operational-truth surface has real implementation beyond the probe script.
---
# No reusable capability
This repo was reviewed for the `reuse-surface` capability registry
(REUSE-WP-0017 coverage campaign) and has no capability to register at this
time. See the `reason` field above.

View File

@@ -12,299 +12,17 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
import stat
import sys import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
DEFAULT_BASE = "https://hub.coulomb.social" from ops_hub.bootstrap import ( # noqa: E402
ROOT = Path(__file__).resolve().parents[1] DEFAULT_BASE,
MANIFEST_PATH = ROOT / "seeds" / "ops-hub-manifest.draft.json" BootstrapError,
WIDGETS_PATH = ROOT / "seeds" / "ops-hub-widgets.seed.json" dry_run_bootstrap,
load_secret,
run_bootstrap,
class BootstrapError(RuntimeError):
pass
def load_secret(name: str, file_name: str) -> str:
value = os.environ.get(name, "").strip()
if value:
return value
file_path = os.environ.get(file_name, "").strip()
if file_path:
return Path(file_path).read_text(encoding="utf-8").strip()
return ""
def request_json(
base_url: str,
method: str,
path: str,
token: str | None,
body: dict[str, Any] | None,
*,
expected: set[int],
) -> dict[str, Any]:
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(base_url + path, data=data, method=method)
request.add_header("Accept", "application/json")
request.add_header("User-Agent", "ops-hub-bootstrap/0.1")
if token:
request.add_header("Authorization", f"Bearer {token}")
if body is not None:
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request, timeout=30) as response:
status = response.status
payload = response.read().decode("utf-8")
except urllib.error.HTTPError as error:
payload = error.read().decode("utf-8", errors="replace")
raise BootstrapError(f"{method} {path} failed with HTTP {error.code}: {payload}") from error
if status not in expected:
raise BootstrapError(f"{method} {path} returned HTTP {status}, expected {sorted(expected)}")
if not payload:
return {}
return json.loads(payload)
def list_items(base_url: str, path: str, token: str | None) -> list[dict[str, Any]]:
response = request_json(base_url, "GET", path, token, None, expected={200})
data = response.get("data", [])
if not isinstance(data, list):
raise BootstrapError(f"expected paginated data array from {path}")
return data
def first_by(items: list[dict[str, Any]], key: str, value: Any) -> dict[str, Any] | None:
return next((item for item in items if item.get(key) == value), None)
def load_manifest() -> dict[str, Any]:
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
required = [
"hub",
"manifestVersion",
"declaredWidgetTypes",
"declaredEventTypes",
"declaredAnnotationCategories",
"declaredPolicyScopes",
]
missing = [key for key in required if key not in manifest]
if missing:
raise BootstrapError(f"{MANIFEST_PATH} missing required key(s): {', '.join(missing)}")
return manifest
def load_widgets() -> list[dict[str, Any]]:
widgets = json.loads(WIDGETS_PATH.read_text(encoding="utf-8"))
if not isinstance(widgets, list):
raise BootstrapError(f"{WIDGETS_PATH} must contain a JSON array")
return widgets
def ensure_hub(base_url: str, operator_key: str, manifest: dict[str, Any]) -> dict[str, Any]:
hub_seed = manifest["hub"]
slug = hub_seed["slug"]
existing = first_by(list_items(base_url, "/api/v2/hubs", operator_key), "slug", slug)
if existing:
return {"record": existing, "created": False}
body = {
"slug": slug,
"name": hub_seed["name"],
"domain": hub_seed["domain"],
"hubKind": hub_seed.get("hubKind", "domain"),
"hubFamily": "vsm",
"vsmFunction": "OPS",
"vsmSystem": "1",
}
record = request_json(base_url, "POST", "/api/v2/hubs", operator_key, body, expected={201})
return {"record": record, "created": True}
def manifest_body(manifest: dict[str, Any], hub_id: str | None = None) -> dict[str, Any]:
body: dict[str, Any] = {
"manifestVersion": manifest["manifestVersion"],
"declaredWidgetTypes": manifest["declaredWidgetTypes"],
"declaredEventTypes": manifest["declaredEventTypes"],
"declaredAnnotationCategories": manifest["declaredAnnotationCategories"],
"declaredPolicyScopes": manifest["declaredPolicyScopes"],
"capabilityDescription": manifest.get("capabilityDescription", ""),
"contact": manifest.get("contact", "operator"),
}
if hub_id:
body["hubId"] = hub_id
return body
def ensure_manifest(base_url: str, operator_key: str, hub_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
query = urllib.parse.urlencode({"hubId": hub_id})
manifests = list_items(base_url, f"/api/v2/hub-capability-manifests?{query}", operator_key)
active = first_by(manifests, "status", "active")
if active:
return {"record": active, "created": False, "activated": False}
draft = first_by(manifests, "status", "draft")
if draft:
record = request_json(
base_url,
"PATCH",
f"/api/v2/hub-capability-manifests/{draft['id']}",
operator_key,
manifest_body(manifest),
expected={200},
)
created = False
else:
record = request_json(
base_url,
"POST",
"/api/v2/hub-capability-manifests",
operator_key,
manifest_body(manifest, hub_id),
expected={201},
)
created = True
activated = request_json(
base_url,
"POST",
f"/api/v2/hub-capability-manifests/{record['id']}/activate",
operator_key,
None,
expected={200},
)
return {"record": activated, "created": created, "activated": True}
def ensure_api_consumer(base_url: str, operator_key: str, manifest_id: str) -> dict[str, Any]:
consumers = list_items(base_url, "/api/v2/api-consumers", operator_key)
existing = first_by(consumers, "name", "ops-hub")
if existing:
return {"record": existing, "created": False}
record = request_json(
base_url,
"POST",
"/api/v2/api-consumers",
operator_key,
{
"name": "ops-hub",
"description": "API consumer for the VSM Operations hub",
"hubCapabilityManifestId": manifest_id,
"rateLimitPerMinute": 120,
"quotaPerDay": 50000,
},
expected={201},
)
return {"record": record, "created": True}
def write_runtime_key(full_key: str, output_path: str | None) -> Path:
if output_path:
path = Path(output_path)
path.write_text(full_key, encoding="utf-8")
else:
fd, raw_path = tempfile.mkstemp(prefix="ops-hub-runtime-key-", text=True)
path = Path(raw_path)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(full_key)
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
return path
def ensure_runtime_key(
base_url: str,
operator_key: str,
api_consumer_id: str,
output_path: str | None,
) -> dict[str, Any]:
existing_runtime_key = load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE")
if existing_runtime_key:
return {
"created": False,
"token": existing_runtime_key,
"keyPrefix": existing_runtime_key[:8],
"keyFile": os.environ.get("OPS_HUB_KEY_FILE"),
}
response = request_json(
base_url,
"POST",
f"/api/v2/api-consumers/{api_consumer_id}/api-keys",
operator_key,
{"scopes": "framework:read hub:ops-hub:read hub:ops-hub:write"},
expected={201},
)
full_key = response.get("fullKey")
if not full_key:
raise BootstrapError("api key creation did not return display-once fullKey")
key_file = write_runtime_key(full_key, output_path)
api_key = response.get("apiKey") or {}
return {
"created": True,
"token": full_key,
"keyPrefix": api_key.get("keyPrefix", full_key[:8]),
"keyFile": str(key_file),
}
def ensure_widgets(base_url: str, runtime_key: str, hub_id: str, widget_seeds: list[dict[str, Any]]) -> dict[str, Any]:
existing_widgets = list_items(base_url, "/api/v2/widgets", runtime_key)
existing_by_ref = {
widget.get("capabilityRef"): widget
for widget in existing_widgets
if widget.get("hubId") == hub_id and widget.get("capabilityRef")
}
created: list[dict[str, Any]] = []
reused: list[dict[str, Any]] = []
for seed in widget_seeds:
existing = existing_by_ref.get(seed.get("capabilityRef"))
if existing:
reused.append(existing)
continue
body = {"hubId": hub_id, "status": "active", **seed}
created.append(request_json(base_url, "POST", "/api/v2/widgets", runtime_key, body, expected={201}))
return {"created": created, "reused": reused}
def submit_gitea_event(base_url: str, runtime_key: str, widgets: dict[str, Any]) -> dict[str, Any] | None:
all_widgets = widgets["created"] + widgets["reused"]
readiness = next(
(widget for widget in all_widgets if widget.get("capabilityRef") == "ops:readiness:gitea-registry"),
None,
)
if not readiness:
return None
return request_json(
base_url,
"POST",
"/api/v2/interaction-events",
runtime_key,
{
"widgetId": readiness["id"],
"eventType": "ops-endpoint-verified",
"viewContext": "railiance-apps/workplans/RAIL-AP-WP-0001",
"metadata": {
"vsmFunction": "OPS",
"vsmSystem": "S1",
"endpoint": "https://gitea.coulomb.social/v2/",
"expectedStatus": 401,
"observedHeader": "Docker-Distribution-Api-Version: registry/2.0",
"recordedBy": "ops-hub/scripts/ops-hub-bootstrap-api.py",
"recordedAt": int(time.time()),
},
},
expected={201},
) )
@@ -312,6 +30,7 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=os.environ.get("IHUB_BASE", DEFAULT_BASE).rstrip("/")) parser.add_argument("--base", default=os.environ.get("IHUB_BASE", DEFAULT_BASE).rstrip("/"))
parser.add_argument("--runtime-key-output", default=os.environ.get("OPS_HUB_RUNTIME_KEY_OUTPUT")) parser.add_argument("--runtime-key-output", default=os.environ.get("OPS_HUB_RUNTIME_KEY_OUTPUT"))
parser.add_argument("--dry-run", action="store_true", help="Plan bootstrap actions without mutating Inter-Hub")
parser.add_argument("--skip-event", action="store_true", help="Do not submit the initial Gitea readiness event") parser.add_argument("--skip-event", action="store_true", help="Do not submit the initial Gitea readiness event")
return parser return parser
@@ -323,39 +42,15 @@ def main() -> int:
print("ERROR: set IHUB_OPERATOR_KEY or IHUB_OPERATOR_KEY_FILE", file=sys.stderr) print("ERROR: set IHUB_OPERATOR_KEY or IHUB_OPERATOR_KEY_FILE", file=sys.stderr)
return 2 return 2
manifest = load_manifest() if args.dry_run:
widget_seeds = load_widgets() summary = dry_run_bootstrap(args.base, operator_key, skip_event=args.skip_event)
else:
hub = ensure_hub(args.base, operator_key, manifest) summary = run_bootstrap(
hub_record = hub["record"] args.base,
manifest_result = ensure_manifest(args.base, operator_key, hub_record["id"], manifest) operator_key,
manifest_record = manifest_result["record"] runtime_key_output=args.runtime_key_output,
consumer = ensure_api_consumer(args.base, operator_key, manifest_record["id"]) skip_event=args.skip_event,
runtime_key = ensure_runtime_key(args.base, operator_key, consumer["record"]["id"], args.runtime_key_output) )
widgets = ensure_widgets(args.base, runtime_key["token"], hub_record["id"], widget_seeds)
event = None if args.skip_event else submit_gitea_event(args.base, runtime_key["token"], widgets)
summary = {
"ok": True,
"base": args.base,
"hub": {"id": hub_record["id"], "slug": hub_record["slug"], "created": hub["created"]},
"manifest": {
"id": manifest_record["id"],
"status": manifest_record["status"],
"created": manifest_result["created"],
"activated": manifest_result["activated"],
},
"apiConsumer": {"id": consumer["record"]["id"], "name": consumer["record"]["name"], "created": consumer["created"]},
"runtimeKey": {
"created": runtime_key["created"],
"keyPrefix": runtime_key["keyPrefix"],
"keyFile": runtime_key["keyFile"],
"storeImmediatelyInOpenBao": "platform/operators/ops-hub/runtime",
"field": "OPS_HUB_KEY",
},
"widgets": {"created": len(widgets["created"]), "reused": len(widgets["reused"])},
"event": None if event is None else {"id": event["id"], "eventType": event["eventType"], "widgetId": event["widgetId"]},
}
print(json.dumps(summary, indent=2, sort_keys=True)) print(json.dumps(summary, indent=2, sort_keys=True))
return 0 return 0

534
src/ops_hub/bootstrap.py Normal file
View File

@@ -0,0 +1,534 @@
"""Authenticated Inter-Hub bootstrap client for ops-hub."""
from __future__ import annotations
import json
import os
import stat
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from ops_hub.interhub_gate_probe import probe_interhub_gate
DEFAULT_BASE = "https://hub.coulomb.social"
ROOT = Path(__file__).resolve().parents[2]
MANIFEST_PATH = ROOT / "seeds" / "ops-hub-manifest.draft.json"
WIDGETS_PATH = ROOT / "seeds" / "ops-hub-widgets.seed.json"
class BootstrapError(RuntimeError):
pass
@dataclass(frozen=True)
class BootstrapPlan:
gate_passed: bool
hub: dict[str, Any]
manifest: dict[str, Any]
api_consumer: dict[str, Any]
runtime_key: dict[str, Any]
widgets: dict[str, Any]
event: dict[str, Any] | None
def load_secret(name: str, file_name: str) -> str:
value = os.environ.get(name, "").strip()
if value:
return value
file_path = os.environ.get(file_name, "").strip()
if file_path:
return Path(file_path).read_text(encoding="utf-8").strip()
return ""
def request_json(
base_url: str,
method: str,
path: str,
token: str | None,
body: dict[str, Any] | None,
*,
expected: set[int],
) -> dict[str, Any]:
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(base_url + path, data=data, method=method)
request.add_header("Accept", "application/json")
request.add_header("User-Agent", "ops-hub-bootstrap/0.1")
if token:
request.add_header("Authorization", f"Bearer {token}")
if body is not None:
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request, timeout=30) as response:
status = response.status
payload = response.read().decode("utf-8")
except urllib.error.HTTPError as error:
payload = error.read().decode("utf-8", errors="replace")
raise BootstrapError(f"{method} {path} failed with HTTP {error.code}: {payload}") from error
if status not in expected:
raise BootstrapError(f"{method} {path} returned HTTP {status}, expected {sorted(expected)}")
if not payload:
return {}
return json.loads(payload)
def list_items(base_url: str, path: str, token: str | None) -> list[dict[str, Any]]:
response = request_json(base_url, "GET", path, token, None, expected={200})
data = response.get("data", [])
if not isinstance(data, list):
raise BootstrapError(f"expected paginated data array from {path}")
return data
def first_by(items: list[dict[str, Any]], key: str, value: Any) -> dict[str, Any] | None:
return next((item for item in items if item.get(key) == value), None)
def load_manifest(path: Path = MANIFEST_PATH) -> dict[str, Any]:
manifest = json.loads(path.read_text(encoding="utf-8"))
required = [
"hub",
"manifestVersion",
"declaredWidgetTypes",
"declaredEventTypes",
"declaredAnnotationCategories",
"declaredPolicyScopes",
]
missing = [key for key in required if key not in manifest]
if missing:
raise BootstrapError(f"{path} missing required key(s): {', '.join(missing)}")
return manifest
def load_widgets(path: Path = WIDGETS_PATH) -> list[dict[str, Any]]:
widgets = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(widgets, list):
raise BootstrapError(f"{path} must contain a JSON array")
return widgets
def find_hub(base_url: str, operator_key: str | None, slug: str) -> dict[str, Any] | None:
tokens: list[str | None] = [None]
if operator_key:
tokens.append(operator_key)
for token in tokens:
try:
existing = first_by(list_items(base_url, "/api/v2/hubs", token), "slug", slug)
except BootstrapError:
continue
if existing:
return existing
return None
def plan_hub(base_url: str, operator_key: str, manifest: dict[str, Any]) -> dict[str, Any]:
slug = manifest["hub"]["slug"]
existing = find_hub(base_url, operator_key, slug)
if existing:
return {"action": "reuse", "record": existing}
return {"action": "create", "record": manifest["hub"]}
def plan_manifest(
base_url: str,
operator_key: str,
hub_id: str,
manifest: dict[str, Any],
) -> dict[str, Any]:
query = urllib.parse.urlencode({"hubId": hub_id})
manifests = list_items(base_url, f"/api/v2/hub-capability-manifests?{query}", operator_key)
active = first_by(manifests, "status", "active")
if active:
return {"action": "reuse", "record": active, "activate": False}
draft = first_by(manifests, "status", "draft")
if draft:
return {"action": "update", "record": draft, "activate": True}
return {"action": "create", "record": manifest_body(manifest, hub_id), "activate": True}
def plan_api_consumer(base_url: str, operator_key: str) -> dict[str, Any]:
existing = first_by(list_items(base_url, "/api/v2/api-consumers", operator_key), "name", "ops-hub")
if existing:
return {"action": "reuse", "record": existing}
return {"action": "create", "record": {"name": "ops-hub"}}
def plan_runtime_key(api_consumer_id: str) -> dict[str, Any]:
existing_runtime_key = load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE")
if existing_runtime_key:
return {
"action": "reuse",
"apiConsumerId": api_consumer_id,
"keyPrefix": existing_runtime_key[:8],
}
return {"action": "create", "apiConsumerId": api_consumer_id}
def plan_widgets(
base_url: str,
runtime_key: str | None,
hub_id: str,
widget_seeds: list[dict[str, Any]],
) -> dict[str, Any]:
if not runtime_key:
return {
"action": "plan",
"create": [seed.get("capabilityRef") for seed in widget_seeds],
"reuse": [],
}
existing_widgets = list_items(base_url, "/api/v2/widgets", runtime_key)
existing_by_ref = {
widget.get("capabilityRef"): widget
for widget in existing_widgets
if widget.get("hubId") == hub_id and widget.get("capabilityRef")
}
create = [seed for seed in widget_seeds if seed.get("capabilityRef") not in existing_by_ref]
reuse = [existing_by_ref[seed["capabilityRef"]] for seed in widget_seeds if seed.get("capabilityRef") in existing_by_ref]
return {"action": "plan", "create": create, "reuse": reuse}
def plan_event(widgets: dict[str, Any], skip_event: bool) -> dict[str, Any] | None:
if skip_event:
return None
all_widgets = widgets.get("reuse", []) + widgets.get("create", [])
readiness = next(
(widget for widget in all_widgets if widget.get("capabilityRef") == "ops:readiness:gitea-registry"),
None,
)
if readiness:
return {"action": "submit", "widgetId": readiness.get("id"), "eventType": "ops-endpoint-verified"}
return {"action": "submit", "widgetId": None, "eventType": "ops-endpoint-verified"}
def build_plan(
base_url: str,
operator_key: str,
*,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> BootstrapPlan:
manifest = manifest or load_manifest()
widget_seeds = widget_seeds or load_widgets()
gate = probe_interhub_gate(base_url)
if not gate.passed:
raise BootstrapError("Inter-Hub bootstrap gate is not open")
hub_plan = plan_hub(base_url, operator_key, manifest)
hub_id = hub_plan["record"]["id"] if hub_plan["action"] == "reuse" else "<new>"
manifest_plan = (
plan_manifest(base_url, operator_key, hub_plan["record"]["id"], manifest)
if hub_plan["action"] == "reuse"
else {"action": "create", "record": manifest_body(manifest), "activate": True}
)
consumer_plan = plan_api_consumer(base_url, operator_key)
consumer_id = consumer_plan["record"].get("id", "<new>")
runtime_plan = plan_runtime_key(consumer_id)
widgets_plan = plan_widgets(
base_url,
load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE") or None,
hub_id if hub_plan["action"] == "reuse" else "<new>",
widget_seeds,
)
event_plan = plan_event(widgets_plan, skip_event)
return BootstrapPlan(
gate_passed=gate.passed,
hub=hub_plan,
manifest=manifest_plan,
api_consumer=consumer_plan,
runtime_key=runtime_plan,
widgets=widgets_plan,
event=event_plan,
)
def manifest_body(manifest: dict[str, Any], hub_id: str | None = None) -> dict[str, Any]:
body: dict[str, Any] = {
"manifestVersion": manifest["manifestVersion"],
"declaredWidgetTypes": manifest["declaredWidgetTypes"],
"declaredEventTypes": manifest["declaredEventTypes"],
"declaredAnnotationCategories": manifest["declaredAnnotationCategories"],
"declaredPolicyScopes": manifest["declaredPolicyScopes"],
"capabilityDescription": manifest.get("capabilityDescription", ""),
"contact": manifest.get("contact", "operator"),
}
if hub_id:
body["hubId"] = hub_id
return body
def ensure_hub(base_url: str, operator_key: str, manifest: dict[str, Any]) -> dict[str, Any]:
hub_seed = manifest["hub"]
slug = hub_seed["slug"]
existing = find_hub(base_url, operator_key, slug)
if existing:
return {"record": existing, "created": False}
body = {
"slug": slug,
"name": hub_seed["name"],
"domain": hub_seed["domain"],
"hubKind": hub_seed.get("hubKind", "domain"),
"hubFamily": "vsm",
"vsmFunction": "OPS",
"vsmSystem": "1",
}
record = request_json(base_url, "POST", "/api/v2/hubs", operator_key, body, expected={201})
return {"record": record, "created": True}
def ensure_manifest(base_url: str, operator_key: str, hub_id: str, manifest: dict[str, Any]) -> dict[str, Any]:
query = urllib.parse.urlencode({"hubId": hub_id})
manifests = list_items(base_url, f"/api/v2/hub-capability-manifests?{query}", operator_key)
active = first_by(manifests, "status", "active")
if active:
return {"record": active, "created": False, "activated": False}
draft = first_by(manifests, "status", "draft")
if draft:
record = request_json(
base_url,
"PATCH",
f"/api/v2/hub-capability-manifests/{draft['id']}",
operator_key,
manifest_body(manifest),
expected={200},
)
created = False
else:
record = request_json(
base_url,
"POST",
"/api/v2/hub-capability-manifests",
operator_key,
manifest_body(manifest, hub_id),
expected={201},
)
created = True
activated = request_json(
base_url,
"POST",
f"/api/v2/hub-capability-manifests/{record['id']}/activate",
operator_key,
None,
expected={200},
)
return {"record": activated, "created": created, "activated": True}
def ensure_api_consumer(base_url: str, operator_key: str, manifest_id: str) -> dict[str, Any]:
consumers = list_items(base_url, "/api/v2/api-consumers", operator_key)
existing = first_by(consumers, "name", "ops-hub")
if existing:
return {"record": existing, "created": False}
record = request_json(
base_url,
"POST",
"/api/v2/api-consumers",
operator_key,
{
"name": "ops-hub",
"description": "API consumer for the VSM Operations hub",
"hubCapabilityManifestId": manifest_id,
"rateLimitPerMinute": 120,
"quotaPerDay": 50000,
},
expected={201},
)
return {"record": record, "created": True}
def write_runtime_key(full_key: str, output_path: str | None) -> Path:
if output_path:
path = Path(output_path)
path.write_text(full_key, encoding="utf-8")
else:
fd, raw_path = tempfile.mkstemp(prefix="ops-hub-runtime-key-", text=True)
path = Path(raw_path)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(full_key)
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
return path
def ensure_runtime_key(
base_url: str,
operator_key: str,
api_consumer_id: str,
output_path: str | None,
) -> dict[str, Any]:
existing_runtime_key = load_secret("OPS_HUB_KEY", "OPS_HUB_KEY_FILE")
if existing_runtime_key:
return {
"created": False,
"token": existing_runtime_key,
"keyPrefix": existing_runtime_key[:8],
"keyFile": os.environ.get("OPS_HUB_KEY_FILE"),
}
response = request_json(
base_url,
"POST",
f"/api/v2/api-consumers/{api_consumer_id}/api-keys",
operator_key,
{"scopes": "framework:read hub:ops-hub:read hub:ops-hub:write"},
expected={201},
)
full_key = response.get("fullKey")
if not full_key:
raise BootstrapError("api key creation did not return display-once fullKey")
key_file = write_runtime_key(full_key, output_path)
api_key = response.get("apiKey") or {}
return {
"created": True,
"token": full_key,
"keyPrefix": api_key.get("keyPrefix", full_key[:8]),
"keyFile": str(key_file),
}
def ensure_widgets(base_url: str, runtime_key: str, hub_id: str, widget_seeds: list[dict[str, Any]]) -> dict[str, Any]:
existing_widgets = list_items(base_url, "/api/v2/widgets", runtime_key)
existing_by_ref = {
widget.get("capabilityRef"): widget
for widget in existing_widgets
if widget.get("hubId") == hub_id and widget.get("capabilityRef")
}
created: list[dict[str, Any]] = []
reused: list[dict[str, Any]] = []
for seed in widget_seeds:
existing = existing_by_ref.get(seed.get("capabilityRef"))
if existing:
reused.append(existing)
continue
body = {"hubId": hub_id, "status": "active", **seed}
created.append(request_json(base_url, "POST", "/api/v2/widgets", runtime_key, body, expected={201}))
return {"created": created, "reused": reused}
def submit_gitea_event(base_url: str, runtime_key: str, widgets: dict[str, Any]) -> dict[str, Any] | None:
all_widgets = widgets["created"] + widgets["reused"]
readiness = next(
(widget for widget in all_widgets if widget.get("capabilityRef") == "ops:readiness:gitea-registry"),
None,
)
if not readiness:
return None
return request_json(
base_url,
"POST",
"/api/v2/interaction-events",
runtime_key,
{
"widgetId": readiness["id"],
"eventType": "ops-endpoint-verified",
"viewContext": "railiance-apps/workplans/RAIL-AP-WP-0001",
"metadata": {
"vsmFunction": "OPS",
"vsmSystem": "S1",
"endpoint": "https://gitea.coulomb.social/v2/",
"expectedStatus": 401,
"observedHeader": "Docker-Distribution-Api-Version: registry/2.0",
"recordedBy": "ops-hub/scripts/ops-hub-bootstrap-api.py",
"recordedAt": int(time.time()),
},
},
expected={201},
)
def run_bootstrap(
base_url: str,
operator_key: str,
*,
runtime_key_output: str | None = None,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
gate = probe_interhub_gate(base_url)
if not gate.passed:
raise BootstrapError("Inter-Hub bootstrap gate is not open")
manifest = manifest or load_manifest()
widget_seeds = widget_seeds or load_widgets()
hub = ensure_hub(base_url, operator_key, manifest)
hub_record = hub["record"]
manifest_result = ensure_manifest(base_url, operator_key, hub_record["id"], manifest)
manifest_record = manifest_result["record"]
consumer = ensure_api_consumer(base_url, operator_key, manifest_record["id"])
runtime_key = ensure_runtime_key(base_url, operator_key, consumer["record"]["id"], runtime_key_output)
widgets = ensure_widgets(base_url, runtime_key["token"], hub_record["id"], widget_seeds)
event = None if skip_event else submit_gitea_event(base_url, runtime_key["token"], widgets)
return {
"ok": True,
"dryRun": False,
"base": base_url,
"gate": {"passed": gate.passed, "hubsStatus": gate.hubs_status},
"hub": {"id": hub_record["id"], "slug": hub_record["slug"], "created": hub["created"]},
"manifest": {
"id": manifest_record["id"],
"status": manifest_record["status"],
"created": manifest_result["created"],
"activated": manifest_result["activated"],
},
"apiConsumer": {"id": consumer["record"]["id"], "name": consumer["record"]["name"], "created": consumer["created"]},
"runtimeKey": {
"created": runtime_key["created"],
"keyPrefix": runtime_key["keyPrefix"],
"keyFile": runtime_key["keyFile"],
"storeImmediatelyInOpenBao": "platform/operators/ops-hub/runtime",
"field": "OPS_HUB_KEY",
},
"widgets": {"created": len(widgets["created"]), "reused": len(widgets["reused"])},
"event": None
if event is None
else {"id": event["id"], "eventType": event["eventType"], "widgetId": event["widgetId"]},
}
def dry_run_bootstrap(
base_url: str,
operator_key: str,
*,
skip_event: bool = False,
manifest: dict[str, Any] | None = None,
widget_seeds: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
gate = probe_interhub_gate(base_url)
plan = build_plan(
base_url,
operator_key,
skip_event=skip_event,
manifest=manifest,
widget_seeds=widget_seeds,
)
return {
"ok": True,
"dryRun": True,
"base": base_url,
"gate": {"passed": gate.passed, "hubsStatus": gate.hubs_status},
"plan": {
"hub": plan.hub,
"manifest": plan.manifest,
"apiConsumer": plan.api_consumer,
"runtimeKey": plan.runtime_key,
"widgets": {
"create": len(plan.widgets.get("create", [])),
"reuse": len(plan.widgets.get("reuse", [])),
},
"event": plan.event,
},
}

View File

@@ -82,7 +82,7 @@ def probe_interhub_gate(base_url: str, timeout: float = 10.0) -> GateResult:
hubs = observe_status(api_url(normalized, "/api/v2/hubs"), timeout) hubs = observe_status(api_url(normalized, "/api/v2/hubs"), timeout)
openapi_observation, openapi = fetch_json(api_url(normalized, "/api/v2/openapi.json"), timeout) openapi_observation, openapi = fetch_json(api_url(normalized, "/api/v2/openapi.json"), timeout)
present, missing = evaluate_openapi_paths(openapi) present, missing = evaluate_openapi_paths(openapi)
passed = hubs.status == 401 and not missing passed = hubs.status in {200, 401} and not missing
return GateResult( return GateResult(
base_url=normalized.rstrip("/"), base_url=normalized.rstrip("/"),
passed=passed, passed=passed,

96
tests/test_bootstrap.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import json
import unittest
from pathlib import Path
from unittest.mock import patch
from typing import Any
from ops_hub.bootstrap import (
MANIFEST_PATH,
WIDGETS_PATH,
find_hub,
load_manifest,
load_widgets,
manifest_body,
plan_api_consumer,
plan_event,
plan_hub,
plan_runtime_key,
)
class BootstrapTests(unittest.TestCase):
def test_load_manifest_and_widgets(self) -> None:
manifest = load_manifest()
widgets = load_widgets()
self.assertEqual(manifest["hub"]["slug"], "ops-hub")
self.assertGreaterEqual(len(widgets), 1)
def test_manifest_body_includes_hub_id_when_provided(self) -> None:
manifest = load_manifest()
body = manifest_body(manifest, "hub-123")
self.assertEqual(body["hubId"], "hub-123")
self.assertIn("declaredWidgetTypes", body)
def test_find_hub_uses_public_discovery_before_operator_token(self) -> None:
calls: list[str | None] = []
def fake_list_items(base_url: str, path: str, token: str | None) -> list[dict[str, Any]]:
calls.append(token)
if token is None:
return [{"slug": "ops-hub", "id": "hub-public"}]
return []
with patch("ops_hub.bootstrap.list_items", side_effect=fake_list_items):
found = find_hub("https://hub.example", "operator-key", "ops-hub")
self.assertEqual(found["id"], "hub-public")
self.assertEqual(calls, [None])
def test_plan_hub_reports_create_when_missing(self) -> None:
manifest = load_manifest()
with patch("ops_hub.bootstrap.find_hub", return_value=None):
plan = plan_hub("https://hub.example", "operator-key", manifest)
self.assertEqual(plan["action"], "create")
def test_plan_hub_reports_reuse_when_present(self) -> None:
manifest = load_manifest()
existing = {"id": "hub-1", "slug": "ops-hub"}
with patch("ops_hub.bootstrap.find_hub", return_value=existing):
plan = plan_hub("https://hub.example", "operator-key", manifest)
self.assertEqual(plan["action"], "reuse")
self.assertEqual(plan["record"]["id"], "hub-1")
def test_plan_api_consumer_reports_reuse(self) -> None:
with patch(
"ops_hub.bootstrap.list_items",
return_value=[{"id": "consumer-1", "name": "ops-hub"}],
):
plan = plan_api_consumer("https://hub.example", "operator-key")
self.assertEqual(plan["action"], "reuse")
def test_plan_runtime_key_reports_reuse_from_env(self) -> None:
with patch.dict("os.environ", {"OPS_HUB_KEY": "runtime-key-value"}, clear=False):
plan = plan_runtime_key("consumer-1")
self.assertEqual(plan["action"], "reuse")
self.assertEqual(plan["keyPrefix"], "runtime-")
def test_plan_event_targets_gitea_readiness_widget(self) -> None:
widgets = {
"reuse": [{"id": "widget-1", "capabilityRef": "ops:readiness:gitea-registry"}],
"create": [],
}
event = plan_event(widgets, skip_event=False)
self.assertIsNotNone(event)
assert event is not None
self.assertEqual(event["widgetId"], "widget-1")
self.assertEqual(event["eventType"], "ops-endpoint-verified")
def test_seed_files_are_valid_json(self) -> None:
json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
json.loads(WIDGETS_PATH.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,54 +0,0 @@
---
id: OPS-WP-0001
type: workplan
title: "Bootstrap State Hub integration"
domain: inter_hub
repo: ops-hub
status: ready
owner: codex
topic_slug: inter_hub
created: "2026-06-06"
updated: "2026-06-06"
---
# Bootstrap State Hub integration
Inter-hub extension for the operations & resiliance subdimension of the orthogonal architecture standard perspective.
## Review Generated Integration Files
```task
id: OPS-WP-0001-T01
status: todo
priority: high
```
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
## Verify Local Developer Workflow
```task
id: OPS-WP-0001-T02
status: todo
priority: high
```
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
## Seed First Real Workplan
```task
id: OPS-WP-0001-T03
status: todo
priority: medium
```
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run from `~/state-hub`:
```bash
make fix-consistency REPO=ops-hub
```

View File

@@ -0,0 +1,76 @@
---
id: OPS-WP-0001
type: workplan
title: "Bootstrap State Hub integration"
domain: infotech
repo: ops-hub
status: archived
owner: codex
topic_slug: inter_hub
created: "2026-06-06"
updated: "2026-07-07"
state_hub_workstream_id: "636b6196-742a-4d09-8b8c-152a4686ad35"
---
# Bootstrap State Hub integration
Bootstrap this repo's State Hub integration and replace generated placeholders
with the first concrete `ops-hub` operating frame.
## Review Generated Integration Files
```task
id: OPS-WP-0001-T01
status: done
priority: high
state_hub_task_id: "c102264b-f104-4e04-ae84-3db441e60659"
```
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
Completed 2026-06-06: `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `README.md`
now describe `ops-hub` as the Operations / System 1 Inter-Hub extension.
Refreshed 2026-07-07: filled `.claude/rules/stack-and-commands.md`,
`architecture.md`, `repo-boundary.md`, updated `SCOPE.md` current state, and
added repo-native dev commands to `AGENTS.md`.
## Verify Local Developer Workflow
```task
id: OPS-WP-0001-T02
status: done
priority: high
state_hub_task_id: "39b276bc-956a-497b-a5d9-f9d5b551aa86"
```
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
Completed 2026-06-06: initial bootstrap had no source tree.
Refreshed 2026-07-07 after OPS-WP-0002: `pyproject.toml`, `src/ops_hub/`,
`tests/`, and `Makefile` provide `make test`, `make interhub-gate`, and
`make interhub-bootstrap` targets documented in `AGENTS.md` and
`.claude/rules/stack-and-commands.md`.
## Seed First Real Workplan
```task
id: OPS-WP-0001-T03
status: done
priority: medium
state_hub_task_id: "dbb77b70-076f-4ff5-b77b-16daf683a4c7"
```
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run from `~/state-hub`:
```bash
make fix-consistency REPO=ops-hub
```
Completed 2026-06-06: seeded
`workplans/OPS-WP-0002-interhub-extension-bootstrap.md`.

View File

@@ -0,0 +1,195 @@
---
id: OPS-WP-0002
type: workplan
title: "Bootstrap ops-hub as an Inter-Hub Operations extension"
domain: infotech
repo: ops-hub
status: archived
owner: codex
topic_slug: inter_hub
created: "2026-06-06"
updated: "2026-07-07"
state_hub_workstream_id: "bea9126c-2f58-41d4-90d1-af4d096f4a10"
---
# Bootstrap ops-hub as an Inter-Hub Operations extension
## Goal
Turn the HelixForge `HF-WP-0001` handoff into the first concrete `ops-hub`
implementation track.
`ops-hub` should become the Operations / System 1 Inter-Hub extension for
operational truth: environments, hosts, clusters, services, endpoints,
releases, backups, incidents, risks, runbooks, readiness gates, and migration
waves.
This repo owns domain-specific implementation assets. `inter-hub` remains the
generic framework, registry, authentication, manifest, widget, event, and
bootstrap API substrate.
## Current Gate
As of 2026-07-07, production Inter-Hub exposes the bootstrap API surface at
`https://hub.coulomb.social`. Run `make interhub-gate` to verify the current
gate before bootstrap.
Do not run manual database seeding unless the operator explicitly chooses that
fallback. The preferred bootstrap path is the supported Inter-Hub API.
Gate criteria:
- Unauthenticated `GET /api/v2/hubs` returns `200` (public discovery) or `401`
(auth-gated listing), not `404`.
- OpenAPI lists `/hubs`, `/hub-capability-manifests`, `/api-consumers`, and
`/policy-scopes`.
- The bootstrap/smoke client can create or reuse the `ops-hub` hub, activate
its manifest, create the runtime API consumer/key, seed initial widgets, and
persist the first governed ops event.
## Handoff Sources
- `/home/worsch/helix-forge/workplans/HF-WP-0001-establish-ops-hub-first-extension.md`
- `/home/worsch/helix-forge/wiki/OpsHubInventory.md`
- `/home/worsch/helix-forge/wiki/OpsHubReadinessGates.md`
- `/home/worsch/helix-forge/wiki/OpsHubBootstrapRunbook.md`
- `/home/worsch/helix-forge/wiki/ops-hub-manifest.draft.json`
- `/home/worsch/helix-forge/wiki/ops-hub-widgets.seed.json`
## Port HelixForge Handoff Artifacts
```task
id: OPS-WP-0002-T01
status: done
priority: high
state_hub_task_id: "7cd1eddb-f508-493b-89b1-6d8575bfd3df"
```
Create repo-local docs and seed data for the ops vocabulary, initial inventory,
readiness gates, bootstrap runbook, manifest draft, and widget seed.
Done when the `ops-hub` repo can be understood without opening HelixForge for
routine implementation details. Keep links back to HelixForge for architectural
context.
Completed 2026-06-06:
- Ported initial inventory to `docs/initial-inventory.md`.
- Ported readiness gates to `docs/readiness-gates.md`.
- Ported bootstrap runbook to `docs/bootstrap-runbook.md`.
- Ported manifest and widget seeds to `seeds/`.
- Added `docs/README.md` as the handoff index.
## Define Repository Source Layout
```task
id: OPS-WP-0002-T02
status: done
priority: high
state_hub_task_id: "27d45a1d-813d-482f-8017-3884be4b509e"
```
Choose and create the first source layout for bootstrap/smoke tooling,
collectors, adapters, and tests. Add the repo-native lint, test, build, and run
commands to `AGENTS.md`.
Done when future code changes have an obvious home and a verification command.
Completed 2026-06-06:
- Added `pyproject.toml`.
- Added Python package layout under `src/ops_hub/`.
- Added operator scripts under `scripts/`.
- Added tests under `tests/`.
- Documented current verification commands in `AGENTS.md`.
## Implement Inter-Hub Production Gate Probe
```task
id: OPS-WP-0002-T03
status: done
priority: high
state_hub_task_id: "3ef0493c-6652-4dd2-a794-5e309c6cef88"
```
Build a small probe that checks the public Inter-Hub bootstrap API gate:
- `/api/v2/hubs` response is `200` or `401` unauthenticated.
- OpenAPI lists the required bootstrap paths.
- The result is machine-readable and suitable for a scheduled ops signal later.
Done when the probe can run locally without secrets and reports the current
gate as pass/fail with clear reasons.
Completed 2026-06-06: `scripts/interhub-gate-probe.py` checks unauthenticated
`/api/v2/hubs` status and required OpenAPI bootstrap paths, emits JSON, and
exits nonzero while the gate is closed. Updated 2026-07-07 to accept public hub
discovery (`200`) as well as auth-gated listing (`401`).
## Implement Bootstrap Smoke Client
```task
id: OPS-WP-0002-T04
status: done
priority: high
state_hub_task_id: "1a8fa43a-d524-4852-94dc-2606098002dc"
```
Implement the authenticated bootstrap/smoke client once Inter-Hub production
exposes the supported bootstrap API.
The client should use `IHUB_BASE` and `IHUB_OPERATOR_KEY` and should create or
reuse:
- `ops-hub` hub row
- active capability manifest
- runtime API consumer/key
- initial governed ops widgets
- first `ops-endpoint-verified` event
Done when a dry-run and an attended real run both produce repeatable evidence
without direct DB access.
Completed 2026-07-07:
- Extracted bootstrap logic to `src/ops_hub/bootstrap.py`.
- `scripts/ops-hub-bootstrap-api.py` supports `--dry-run` and `--skip-event`.
- Public hub discovery is attempted before authenticated listing.
- `make interhub-bootstrap-dry-run` and `make test` verify the client locally.
- Attended production run: `make interhub-bootstrap IHUB_OPERATOR_KEY_FILE=...`
## Seed First Operational Signal
```task
id: OPS-WP-0002-T05
status: done
priority: medium
state_hub_task_id: "b7d09ecd-0899-470d-aea5-6e2f07caae96"
```
Submit the first governed ops signal for the Gitea registry endpoint once the
manifest, widget, event type, and API key exist.
Initial signal:
```json
{
"eventType": "ops-endpoint-verified",
"endpoint": "https://gitea.coulomb.social/v2/",
"expectedStatus": 401,
"viewContext": "railiance-apps/workplans/RAIL-AP-WP-0001"
}
```
Done when the event is visible in Inter-Hub and traceable to the owning
Railiance workplan.
Completed 2026-07-07 via `warden access --exec` with `platform-admin` OpenBao
token:
- Hub `ops-hub` (`4f6e4cf7-6a96-4ff2-8a37-08c9f9e405d2`) reused.
- Active manifest `00aaf90a-8e76-4b0e-892d-33b162862f38` reused.
- Event `3c894714-1b90-4cfa-9ce1-340704bd6526` (`ops-endpoint-verified`) on
widget `8c768282-aca6-466b-9c62-c6fc8f2d5624`.
- Runtime key prefix `ch_HfXNc138N` stored at
`platform/operators/ops-hub/runtime` (`OPS_HUB_KEY`).