generated from coulomb/repo-seed
Compare commits
6 Commits
cec94ac00f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 39080333da | |||
|
|
0454e126cb | ||
|
|
ad7f148c36 | ||
| c68553673d | |||
| 057ce6d888 | |||
| 21bfd5fa49 |
20
.claude/rules/agents.md
Normal file
20
.claude/rules/agents.md
Normal 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.
|
||||||
29
.claude/rules/architecture.md
Normal file
29
.claude/rules/architecture.md
Normal 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
|
||||||
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal 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`
|
||||||
38
.claude/rules/first-session.md
Normal file
38
.claude/rules/first-session.md
Normal 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 1–3 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 -->
|
||||||
11
.claude/rules/repo-boundary.md
Normal file
11
.claude/rules/repo-boundary.md
Normal 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
|
||||||
5
.claude/rules/repo-identity.md
Normal file
5
.claude/rules/repo-identity.md
Normal 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
|
||||||
85
.claude/rules/session-protocol.md
Normal file
85
.claude/rules/session-protocol.md
Normal 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.
|
||||||
27
.claude/rules/stack-and-commands.md
Normal file
27
.claude/rules/stack-and-commands.md
Normal 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
|
||||||
|
```
|
||||||
40
.claude/rules/workplan-convention.md
Normal file
40
.claude/rules/workplan-convention.md
Normal 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 -->
|
||||||
@@ -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.
|
||||||
|
|||||||
81
AGENTS.md
81
AGENTS.md
@@ -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
12
CLAUDE.md
Normal 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
|
||||||
25
INTENT.md
25
INTENT.md
@@ -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.
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
40
SCOPE.md
40
SCOPE.md
@@ -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
15
docs/README.md
Normal 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.
|
||||||
@@ -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
146
docs/initial-inventory.md
Normal 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
63
docs/readiness-gates.md
Normal 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.
|
||||||
15
registry/NO_CAPABILITIES.md
Normal file
15
registry/NO_CAPABILITIES.md
Normal 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.
|
||||||
@@ -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
534
src/ops_hub/bootstrap.py
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
96
tests/test_bootstrap.py
Normal 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()
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
76
workplans/archived/260707-OPS-WP-0001-statehub-bootstrap.md
Normal file
76
workplans/archived/260707-OPS-WP-0001-statehub-bootstrap.md
Normal 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`.
|
||||||
@@ -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`).
|
||||||
Reference in New Issue
Block a user