Publish railiance01 resource evidence and close RAIL-HO-WP-0008
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled

Non-secret identity, a reproducible capacity observation, and host-ops
labor/exit inputs for resource:hosteurope:railiance01. Booked price and
contract dates stay unknown for their owners.
This commit is contained in:
codex 2026-08-15 19:04:40 +02:00
parent 39070ed292
commit 0a97ce12be
9 changed files with 669 additions and 8 deletions

View file

@ -270,6 +270,16 @@ verify: ## Run Goss test suite against all hosts, commit TAP reports — exits n
git diff --cached --quiet && echo "No new reports to commit." || \
git commit -m "chore: Goss verification reports $$(date -u +%Y-%m-%dT%H%M%SZ)"
observe-railiance01: ## Timestamped host capacity observation for resource-control
@mkdir -p docs/evidence/resource-hosteurope-railiance01/observations
@out=$$(mktemp); \
python3 scripts/observe-host-capacity.py Railiance01 > $$out; \
stamp=$$(python3 -c "import json; print(json.load(open('$$out'))['observed_at'].replace(':',''))"); \
dest=docs/evidence/resource-hosteurope-railiance01/observations/$$stamp.json; \
mv $$out $$dest; \
ln -sfn $$stamp.json docs/evidence/resource-hosteurope-railiance01/observations/latest.json; \
echo "wrote $$dest"
verify-host: ## Run Goss against one host: make verify-host HOST=Railiance01
@test -n "$(HOST)" || (echo "Usage: make verify-host HOST=Railiance01"; exit 1)
@echo "Running Goss baseline assertions on $(HOST)..."

View file

@ -89,6 +89,9 @@ higher layer (Kubernetes, platform, etc.) can run.
- **Firewall posture**: `RAIL-HO-WP-0009` finished. k3s API is tunnel-only
(ADR-005); live `Railiance01` UFW matches the declaration. CoulombCore
UFW stays unmanaged (`ufw_manage: false`)
- **Resource evidence**: `RAIL-HO-WP-0008` finished. Non-secret identity,
capacity observations, and host-ops labor live under
`docs/evidence/resource-hosteurope-railiance01/`
---

View file

@ -0,0 +1,63 @@
# Evidence interface — `resource:hosteurope:railiance01`
Stable, non-secret facts that `resource-control` should read for the Host
Europe virtual server `Railiance01`. This directory is the interface.
`resource-control` remains the portfolio record; `fin-hub` remains booked
cost. Do not copy invoices or credentials into either side.
## Files
| Path | Task | What it fills |
| --- | --- | --- |
| `identity-lifecycle.yaml` | T01 | provider, product, IDs, location, lifecycle, classified unknowns |
| `observations/*.json` | T02 | timestamped OS / provider / k3s capacity and utilization |
| `observations/latest.json` | T02 | symlink to the newest observation |
| `labor-and-exit.yaml` | T03 | recurring host-ops activities, estimated hours, exit path |
Producer: `scripts/observe-host-capacity.py Railiance01`
```bash
make observe-railiance01
```
Recommended cadence: monthly, and after any provider resize. Traffic
allowance is still unknown (operator / panel).
## How to fold into resource-control
Map, do not duplicate operating strings into a shadow inventory:
| Inventory field | Source |
| --- | --- |
| `provider.name` | Host Europe GmbH |
| `provider.product_ref` | `oh.hosting.c2.r4.d100` |
| `provider.provider_resource_id` | `c0e697d1-4c71-40f5-9746-0b4c4b9e5c17` |
| `provider.account_ref` | `openstack-project:f6491e4f796146d1a7ce30d569d5f122` |
| `location.country` | `FR` |
| `location.region` | `sxb1` |
| `location.residency` | `EU` |
| `lifecycle.commissioned_on` | `2026-03-08` |
| `lifecycle.ordered_on` / `renews_on` / `cancel_by` | remain `null` (unknown, operator) |
| `cost.*` booked price | remain unknown; owner `fin-hub` / `FIN-WP-0004` |
| `capacity[]` | latest observation, kinds `usable` / `provisioned` / `observed` |
| labor hours in forecasts | `labor-and-exit.yaml#monthly_host_ops_hours` (basis `estimated`) |
Cluster utilization and PVC allocation stay `RAIL-BS-WP-0014`.
## Layers in an observation
1. **provider_declared** — instance type and UUIDs from named cloud-init keys.
2. **operating_system** — what the kernel sees (CPU, memory, root filesystem).
3. **kubernetes** — k3s capacity, allocatable, and `kubectl top` when present.
Do not treat k3s allocatable as the provider grant. On 2026-08-15 they
happened to match for CPU and memory; ephemeral-storage allocatable is
smaller than the root filesystem.
## What is still unknown
- Host Europe order date, renewal date, cancellation deadline, contract term
- Billing customer number (restricted)
- Traffic allowance
- Booked recurring price and tax treatment (`fin-hub`)
- Measured operator hours (no time records; estimates only)

View file

@ -0,0 +1,139 @@
# Non-secret identity and lifecycle for resource:hosteurope:railiance01
# Consumer: resource-control inventory fields on that resource.
# Booked price, invoices, customer numbers, and payment instruments are
# out of scope (fin-hub / operator). Unknowns stay unknown.
schema_version: "1.0"
resource_id: resource:hosteurope:railiance01
inventory_name: Railiance01
public_ipv4: 92.205.62.239
status: active
management_model: provider_managed
resource_class: compute_instance
published_at: "2026-08-15"
authority: railiance-infra
workplan: RAIL-HO-WP-0008-T01
provider:
name: Host Europe GmbH
as_number: AS21499
parent: GoDaddy
product_class: virtual server
product_family: vps4
product_ref:
value: oh.hosting.c2.r4.d100
basis: measured
source: cloud-init ds.ec2_metadata.instance-type
note: Provider instance-type string. Not a booked catalogue confirmation.
provider_resource_id:
value: c0e697d1-4c71-40f5-9746-0b4c4b9e5c17
basis: measured
source: cloud-init ds.meta_data.uuid (OpenStack Nova)
provider_instance_id_ec2:
value: i-000197c7
basis: measured
source: cloud-init ds.ec2_metadata.instance-id
instance_name:
value: 1fxw7h4zug
basis: measured
source: cloud-init ds.meta_data.name
account_ref:
value: openstack-project:f6491e4f796146d1a7ce30d569d5f122
basis: measured
source: cloud-init ds.meta_data.project_id
classification: non-secret-join-key
note: Cloud tenant UUID for joins. Not the billing customer number.
billing_customer_number:
value: null
classification: restricted
owner: operator
source: Host Europe control panel
provisioning:
value: manual
basis: quoted
source: resource-control/docs/evidence/RESOURCE-WP-0002-host-europe-resolution-2026-08-15.md
note: Host Europe has no API to provision this class of resource.
location:
country:
value: FR
basis: derived
sources:
- cloud-init local-hostname contains sxb1 (Strasbourg site code)
- ipinfo.io 2026-08-15 city=Strasbourg country=FR org=AS21499 Host Europe GmbH
- Host Europe Group operates a Strasbourg data centre
region:
value: sxb1
basis: measured
source: cloud-init ds.ec2_metadata.local-hostname = 1fxw7h4zug.cloud.prd.sxb1.gdg
locality:
value: Strasbourg
basis: derived
source: sxb1 + ipinfo.io
residency:
value: EU
basis: derived
source: country FR
failure_domains:
- provider:host-europe
- site:sxb1
- host:railiance01
ptr: 239.62.205.92.host.secureserver.net
lifecycle:
proposed_on:
value: null
classification: unknown
owner: operator
ordered_on:
value: null
classification: unknown
owner: operator
source: Host Europe control panel or invoice
commissioned_on:
value: "2026-03-08"
basis: measured
source: workplans/archived/260702-RAIL-HO-WP-0001-hosteurope-bootstrap.md and state-hub-inbox/2026-03-08-railiance01-bootstrap.yaml
note: Date this repo took the host into S1 management. Not the provider order date.
renews_on:
value: null
classification: unknown
owner: operator
source: Host Europe control panel or invoice
cancel_by:
value: null
classification: unknown
owner: operator
source: Host Europe contract / cancellation notice period
contract_term:
value: null
classification: unknown
owner: operator
retired_on:
value: null
classification: not_applicable
note: Resource is active.
cost_boundary:
currency: EUR
booked_price:
value: null
classification: unknown
owner: fin-hub
workplan: FIN-WP-0004
tax_treatment:
value: null
classification: unknown
owner: fin-hub
this_repo_publishes: identity, location, lifecycle dates we can prove, host observations, host-ops labor
this_repo_does_not_publish:
- invoices
- customer numbers
- payment instruments
- booked recurring price
residual_credential:
finding: cloud-init metadata on the host still exposes an admin_pass field
action: rotate or disable that password via the Host Europe panel if it is still valid
recorded: false
note: The value was not copied into this repository.

View file

@ -0,0 +1,101 @@
# Host-level operations labor and exit inputs for resource:hosteurope:railiance01
# Consumer: resource-control forecasts that need to separate host infrastructure,
# recurring host-ops labor, and replacement/exit effort.
# Does not estimate workload, cluster, or platform labor.
schema_version: "1.0"
resource_id: resource:hosteurope:railiance01
published_at: "2026-08-15"
authority: railiance-infra
workplan: RAIL-HO-WP-0008-T03
scope:
includes:
- OS baseline convergence
- host firewall declared state
- host verification (Goss)
- SSH access and host identity
- host incident response
- replacement and cancellation of this virtual server
excludes:
- k3s / cluster operations (railiance-cluster)
- platform services (railiance-platform)
- application and tenant labor (those repos)
- booked Host Europe spend (fin-hub)
recurring_activities:
- id: os-baseline-converge
description: Apply ansible/playbooks/bootstrap.yaml or a tagged subset
cadence: event-driven
typical_hours: 1.0
basis: estimated
note: One tagged run. Full untagged converge is deliberately not routine.
- id: firewall-declared-state
description: Review and apply UFW from inventory (make converge-firewall)
cadence: event-driven
typical_hours: 0.5
basis: estimated
note: Hourly Goss timer is unattended; this is operator time when it fails.
- id: host-verify
description: make verify-host HOST=Railiance01 and read TAP
cadence: monthly
typical_hours: 0.25
basis: estimated
- id: ssh-and-identity
description: SSH CA, authorized_keys, ops-bridge key, inventory IP
cadence: event-driven
typical_hours: 0.5
basis: estimated
- id: host-incident
description: Host-level outage, disk, SSH lockout, provider ticket
cadence: unscheduled
typical_hours: 2.0
basis: estimated
note: Not every month. Use as a rare-event allowance, not a measured mean.
monthly_host_ops_hours:
value: 2.0
unit: hours
basis: estimated
method: Sum of typical_hours for monthly verify plus a share of event-driven work
uncertainty: No time records exist. This is a forecast placeholder, not a measurement.
owner: railiance-infra
one_off_not_recurring:
- id: RAIL-HO-WP-0009
description: Firewall declared-state integrity and k3s API tunnel-only cutover
completed: "2026-08-15"
hours: null
basis: unknown
note: Do not amortise into the monthly host-ops figure until a time record exists.
exit:
path:
- Provision replacement compute (manual Host Europe panel or another provider)
- Restore or redeploy k3s and stateful services from their owning repos
- Switch DNS and ingress
- Verify workloads
- Cancel the Host Europe virtual server in the panel
- Confirm disk wipe / instance destroy
replacement_cutover_hours:
value: 8.0
unit: hours
basis: estimated
uncertainty: Host-ops only. Cluster and workload cutover hours belong to those repos.
cancellation:
notice_period:
value: null
classification: unknown
owner: operator
source: Host Europe contract
panel: Host Europe / GoDaddy VPS control panel
provider_api: none
data_at_rest:
items:
- Ubuntu root filesystem (193 GiB provisioned class)
- local-path PVs for cluster workloads (failure domain host:railiance01)
note: PV contents are not S1-owned; list them only as correlated-exit risk.
cadence_for_consumers:
republish_labor: when the activity catalog changes, or after a quarter of time records
republish_exit: when replacement procedure or cancel terms become known

View file

@ -0,0 +1,92 @@
{
"schema_version": "1.0",
"record_type": "host_capacity_observation",
"resource_id": "resource:hosteurope:railiance01",
"inventory_name": "Railiance01",
"observed_at": "2026-08-15T17:02:50Z",
"observer": "railiance-infra/scripts/observe-host-capacity.py",
"source_commands": [
"nproc / lscpu /proc/meminfo /proc/loadavg statvfs(/)",
"cloud-init query <named keys only>",
"sudo k3s kubectl get node -o json",
"sudo k3s kubectl top node"
],
"layers": {
"provider_declared": {
"instance_type": "oh.hosting.c2.r4.d100",
"instance_uuid": "c0e697d1-4c71-40f5-9746-0b4c4b9e5c17",
"instance_id_ec2": "i-000197c7",
"instance_name": "1fxw7h4zug",
"product_family": "vps4",
"product_role": "vps4_default",
"local_hostname": "1fxw7h4zug.cloud.prd.sxb1.gdg",
"project_id": "f6491e4f796146d1a7ce30d569d5f122",
"note": "Declared by the provider metadata service. Not a booked SKU confirmation."
},
"operating_system": {
"hostname": "239.62.205.92.host.secureserver.net",
"os": "Ubuntu 24.04.4 LTS",
"cpu": {
"value": 4,
"unit": "vCPU",
"kind": "usable",
"model": "AMD EPYC Processor (with IBPB)",
"hypervisor": "KVM"
},
"memory": {
"total_kib": 16377056,
"available_kib": 10577368,
"unit": "KiB",
"kind": "usable"
},
"swap": {
"total_kib": 4194300,
"unit": "KiB",
"kind": "observed",
"note": "OS swapfile, not a provider disk grant"
},
"root_filesystem": {
"total_bytes": 206900281344,
"used_bytes": 77003415552,
"avail_bytes": 129880088576,
"kind": "provisioned"
},
"block_devices": "sda 200G disk QEMU HARDDISK\nsr0 838K rom QEMU DVD-ROM",
"loadavg": [
"1.85",
"1.38",
"1.31"
],
"uptime_seconds": 1738009.47
},
"kubernetes": {
"node_name": "239.62.205.92.host.secureserver.net",
"capacity": {
"cpu": "4",
"memory": "16377056Ki",
"ephemeral_storage": "202051056Ki",
"pods": "110"
},
"allocatable": {
"cpu": "4",
"memory": "16377056Ki",
"ephemeral_storage": "196555267123",
"pods": "110"
},
"observed": {
"cpu": "1715m",
"cpu_pct": "42%",
"memory": "6308Mi",
"memory_pct": "39%"
}
}
},
"unknown": [
{
"field": "traffic_allowance",
"classification": "unknown",
"owner": "operator",
"source": "Host Europe control panel or contract"
}
]
}

View file

@ -0,0 +1 @@
2026-08-15T170250Z.json

216
scripts/observe-host-capacity.py Executable file
View file

@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Collect a non-secret host capacity observation over SSH.
Does not dump cloud-init metadata wholesale (that blob can contain
admin_pass). Only named keys are read.
Usage:
scripts/observe-host-capacity.py Railiance01
scripts/observe-host-capacity.py Railiance01 > docs/evidence/resource-hosteurope-railiance01/observations/....json
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
REMOTE = r"""
set -euo pipefail
python3 - <<'PY'
import json, os, subprocess, shutil
def sh(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()
def cloud(key):
if not shutil.which("cloud-init"):
return None
try:
out = subprocess.check_output(
["cloud-init", "query", key], text=True, stderr=subprocess.DEVNULL
).strip()
except subprocess.CalledProcessError:
return None
if not out or out.lower() in {"none", "null"}:
return None
return out
mem = open("/proc/meminfo").read().split()
kv = {}
for i, tok in enumerate(mem):
if tok.endswith(":") and i + 1 < len(mem):
try:
kv[tok[:-1]] = int(mem[i + 1])
except ValueError:
pass
st = os.statvfs("/")
k3s = {}
if shutil.which("k3s"):
raw = subprocess.check_output(
["sudo", "k3s", "kubectl", "get", "node", "-o", "json"], text=True
)
node = json.loads(raw)["items"][0]
cap = node["status"]["capacity"]
alloc = node["status"]["allocatable"]
k3s = {
"node_name": node["metadata"]["name"],
"capacity": {
"cpu": cap.get("cpu"),
"memory": cap.get("memory"),
"ephemeral_storage": cap.get("ephemeral-storage"),
"pods": cap.get("pods"),
},
"allocatable": {
"cpu": alloc.get("cpu"),
"memory": alloc.get("memory"),
"ephemeral_storage": alloc.get("ephemeral-storage"),
"pods": alloc.get("pods"),
},
}
try:
top = subprocess.check_output(
["sudo", "k3s", "kubectl", "top", "node", "--no-headers"],
text=True,
stderr=subprocess.DEVNULL,
).split()
# NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
if len(top) >= 5:
k3s["observed"] = {
"cpu": top[1],
"cpu_pct": top[2],
"memory": top[3],
"memory_pct": top[4],
}
except subprocess.CalledProcessError:
k3s["observed"] = None
print(json.dumps({
"hostname": sh("hostname -f"),
"os_pretty": sh("grep ^PRETTY_NAME= /etc/os-release").split("=", 1)[1].strip('"'),
"nproc": int(sh("nproc")),
"cpu_model": sh("lscpu | awk -F: '/Model name/ {print $2; exit}'").strip(),
"hypervisor": sh("lscpu | awk -F: '/Hypervisor vendor/ {print $2; exit}'").strip() or None,
"mem_total_kib": kv.get("MemTotal"),
"mem_available_kib": kv.get("MemAvailable"),
"swap_total_kib": kv.get("SwapTotal"),
"root_total_bytes": st.f_frsize * st.f_blocks,
"root_used_bytes": st.f_frsize * (st.f_blocks - st.f_bfree),
"root_avail_bytes": st.f_frsize * st.f_bavail,
"block_devices": sh("lsblk -dn -o NAME,SIZE,TYPE,MODEL"),
"loadavg": open("/proc/loadavg").read().split()[:3],
"uptime_seconds": float(open("/proc/uptime").read().split()[0]),
"instance_type": cloud("ds.ec2_metadata.instance-type"),
"instance_id_ec2": cloud("ds.ec2_metadata.instance-id"),
"instance_uuid": cloud("ds.meta_data.uuid"),
"instance_name": cloud("ds.meta_data.name"),
"product_family": cloud("ds.meta_data.meta.app"),
"product_role": cloud("ds.meta_data.meta.role"),
"local_hostname": cloud("ds.ec2_metadata.local-hostname"),
"project_id": cloud("ds.meta_data.project_id"),
"k3s": k3s or None,
}))
PY
"""
def inventory_ssh(name: str) -> tuple[str, str]:
import yaml
path = os.path.join(os.path.dirname(__file__), "..", "inventory", "servers.yaml")
with open(path) as f:
servers = yaml.safe_load(f).get("servers") or []
for s in servers:
if s.get("name") == name:
return s["ip"], s.get("ssh_user", "tegwick")
raise SystemExit(f"unknown inventory host: {name}")
def main() -> int:
if len(sys.argv) != 2:
print("Usage: observe-host-capacity.py <InventoryName>", file=sys.stderr)
return 2
name = sys.argv[1]
ip, user = inventory_ssh(name)
observed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
raw = subprocess.check_output(
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", f"{user}@{ip}", REMOTE],
text=True,
)
host = json.loads(raw)
rec = {
"schema_version": "1.0",
"record_type": "host_capacity_observation",
"resource_id": "resource:hosteurope:railiance01" if name == "Railiance01" else f"resource:host:{name.lower()}",
"inventory_name": name,
"observed_at": observed_at,
"observer": "railiance-infra/scripts/observe-host-capacity.py",
"source_commands": [
"nproc / lscpu /proc/meminfo /proc/loadavg statvfs(/)",
"cloud-init query <named keys only>",
"sudo k3s kubectl get node -o json",
"sudo k3s kubectl top node",
],
"layers": {
"provider_declared": {
"instance_type": host.get("instance_type"),
"instance_uuid": host.get("instance_uuid"),
"instance_id_ec2": host.get("instance_id_ec2"),
"instance_name": host.get("instance_name"),
"product_family": host.get("product_family"),
"product_role": host.get("product_role"),
"local_hostname": host.get("local_hostname"),
"project_id": host.get("project_id"),
"note": "Declared by the provider metadata service. Not a booked SKU confirmation.",
},
"operating_system": {
"hostname": host["hostname"],
"os": host["os_pretty"],
"cpu": {
"value": host["nproc"],
"unit": "vCPU",
"kind": "usable",
"model": host.get("cpu_model"),
"hypervisor": host.get("hypervisor"),
},
"memory": {
"total_kib": host["mem_total_kib"],
"available_kib": host["mem_available_kib"],
"unit": "KiB",
"kind": "usable",
},
"swap": {
"total_kib": host["swap_total_kib"],
"unit": "KiB",
"kind": "observed",
"note": "OS swapfile, not a provider disk grant",
},
"root_filesystem": {
"total_bytes": host["root_total_bytes"],
"used_bytes": host["root_used_bytes"],
"avail_bytes": host["root_avail_bytes"],
"kind": "provisioned",
},
"block_devices": host["block_devices"],
"loadavg": host["loadavg"],
"uptime_seconds": host["uptime_seconds"],
},
"kubernetes": host.get("k3s"),
},
"unknown": [
{
"field": "traffic_allowance",
"classification": "unknown",
"owner": "operator",
"source": "Host Europe control panel or contract",
}
],
}
json.dump(rec, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -4,11 +4,11 @@ type: workplan
title: "Publish railiance01 resource and commercial evidence"
domain: financials
repo: railiance-infra
status: ready
status: finished
owner: codex
topic_slug: railiance
created: "2026-08-11"
updated: "2026-08-11"
updated: "2026-08-15"
related:
- RESOURCE-WP-0003
state_hub_workstream_id: "7122657f-87c8-46b5-a725-a1af1ba0af12"
@ -27,7 +27,7 @@ Origin: `RESOURCE-WP-0003-T04` delegated evidence gap.
```task
id: RAIL-HO-WP-0008-T01
status: todo
status: done
priority: high
state_hub_task_id: "cde55584-229d-4e27-8fc8-0f54d78c71ed"
```
@ -41,11 +41,23 @@ customer numbers, or payment instruments.
Done when resource-control can update the provider and lifecycle fields with
provenance, while unknown or restricted fields remain explicitly classified.
**Done 2026-08-15.**
`docs/evidence/resource-hosteurope-railiance01/identity-lifecycle.yaml`
- Product class: virtual server, family `vps4`, type `oh.hosting.c2.r4.d100`
- Provider resource ID: OpenStack UUID `c0e697d1-4c71-40f5-9746-0b4c4b9e5c17`
- Join key: `openstack-project:f6491e4f796146d1a7ce30d569d5f122` (not the
billing customer number)
- Location: `sxb1` / Strasbourg / `FR` / EU
- Commissioned into S1 management: `2026-03-08`
- Order, renewal, cancel-by, contract term: unknown, owner operator
- Booked price: unknown, owner fin-hub (`FIN-WP-0004`)
## T02 — Publish host capacity and utilization evidence
```task
id: RAIL-HO-WP-0008-T02
status: todo
status: done
priority: high
state_hub_task_id: "8b0e5547-23ae-4516-b6d7-9c196882d088"
```
@ -58,11 +70,20 @@ Kubernetes observations.
Done when at least one reproducible observation can be consumed by
resource-control and its source and cadence are documented.
**Done 2026-08-15.** Producer
`scripts/observe-host-capacity.py`; first observation
`docs/evidence/resource-hosteurope-railiance01/observations/2026-08-15T170250Z.json`
(also `observations/latest.json`). Cadence: monthly, and after a resize.
Traffic allowance remains unknown.
Sample at `2026-08-15T17:02:50Z`: 4 vCPU, 16377056 KiB RAM, 193 GiB root
(72 GiB used); k3s `kubectl top` 1715m CPU / 6308 MiB memory.
## T03 — Record infrastructure operations labor and exit inputs
```task
id: RAIL-HO-WP-0008-T03
status: todo
status: done
priority: medium
state_hub_task_id: "8bdaa96f-400b-455f-87db-d5c11e287142"
```
@ -74,8 +95,23 @@ Do not estimate another repository's workload labor.
Done when resource-control can separate host infrastructure, recurring labor,
and migration effort in forecasts and optimization cases.
**Done 2026-08-15.**
`docs/evidence/resource-hosteurope-railiance01/labor-and-exit.yaml`
Recurring host-ops catalog plus `monthly_host_ops_hours: 2.0` (basis
`estimated`, no time records). Replacement cutover 8 host-ops hours
(estimated). Cancellation notice period unknown. Cluster and workload labor
excluded.
## Acceptance
- [ ] Provider identity and lifecycle evidence is non-secret and attributable.
- [ ] Capacity and utilization observations have timestamps and provenance.
- [ ] Host labor and exit inputs can be consumed without duplicating booked cost.
- [x] Provider identity and lifecycle evidence is non-secret and attributable.
- [x] Capacity and utilization observations have timestamps and provenance.
- [x] Host labor and exit inputs can be consumed without duplicating booked cost.
## Outcome
Finished 2026-08-15. Interface:
`docs/evidence/resource-hosteurope-railiance01/`. resource-control can fold
the known fields and must leave booked price and contract dates unknown until
their owners publish them.