{
  "available": true,
  "status": "proposed",
  "note": "HOST-004. Elastic burst compute policy and the provider adapter plan. Nothing here provisions anything: the console renders this register and holds no provider credential, and scripts/jarvisos-burst.mjs refuses every billable verb without an owner authorisation grant. Measurements marked 'measured' were taken on VIN on 2026-08-17; every price is arithmetic on a number nobody has yet checked against an invoice, which is gap burst-prices-unverified.",
  "generatedAt": "2026-08-17",
  "policy": {
    "principle": "Transient work gets transient capacity. A build that runs for six hours a month does not justify a machine that runs for 720, and an upgrade bought for a peak is paid for at every trough.",
    "freeFirst": "Zero recurring cost is the default and the rollback plan. Every workload class must have at least one free eligible target, so that switching the elastic options off is one field in this file rather than a migration. Paid capacity is allowed only where a measured operational problem is recorded next to the spend, in paidBecause and paidEvidence.",
    "ephemerality": "A burst node is created, used and destroyed. It holds no durable state, it is reached over the tailnet and never from the public internet, it carries a lease, and teardown means destroy — powering a server off stops the CPU, not the invoice.",
    "credentials": "Provider credentials are never in this file, in any config, in argv, in a log line, in a report or in browser storage. Each is declared by name and location only and resolved at call time from an environment variable or a 0600 file under /var/lib/jarvisos/secrets, outside this working tree and owned by the jarvis service account.",
    "spend": "Every billable operation is attributable to one authorisation grant, one target and one ceiling. An unattributable euro is a bug in this contract, not a rounding error.",
    "maxRecurringEur": 0,
    "monthlyCeilingEur": 10,
    "runCeilingEur": 2,
    "maxLeaseMinutes": 240,
    "referenceHours": 20,
    "referenceHoursNote": "The utilisation an otherwise-unused target is scored at: roughly one transient workload class's worth of monthly hours. Without it an option nothing routes work to costs nothing, and would score top marks on cost for the sole achievement of being idle.",
    "weights": {
      "cost": 0.3,
      "startup": 0.2,
      "control": 0.15,
      "snapshots": 0.1,
      "bandwidth": 0.1,
      "mesh": 0.15
    },
    "weightsNote": "The six axes the task named. Cost leads because the estate's stated optimisation is zero recurring cost; startup and mesh attach are next because a burst target that takes four minutes to become reachable is not usable for interactive work whatever it costs. Snapshots and bandwidth are real but rarely decide anything at this scale."
  },
  "approval": {
    "id": "owner-authorisation",
    "summary": "Read-only inventory and health run unattended because they are free and cannot change anything. Everything that provisions, resizes, rebuilds, snapshots or deletes spends money or destroys something, and every one of those requires a scoped, budget-capped, expiring, single-use grant issued by the owner.",
    "automatic": {
      "rule": "An operation may run unattended if and only if its effect is 'read' and it is not billable. There is no third condition and no exception list.",
      "covers": [
        "inventory: which burst resources exist right now, in which project, with which labels",
        "health: power state, uptime and metrics of a running burst node",
        "pricing: the provider's own price list, which is how every euro on this page gets verified",
        "usage: minutes and traffic consumed so far this month, which is how the ceiling is watched",
        "estimate: arithmetic performed locally on the numbers above"
      ],
      "runsAs": "the read-only credential, which the provider itself refuses on every mutating route",
      "cadence": "on demand and on the reconciliation timer; never more often than once a minute"
    },
    "owner": {
      "rule": "Any operation that is billable, or whose effect is create, mutate or destroy, requires a valid grant. Declaring such an operation automatic fails npm run check.",
      "covers": [
        "provision: create a server, a volume or a floating address",
        "resize: change a server type, in either direction — scaling down still bills the change",
        "rebuild: replace a running server's image",
        "snapshot: create or keep an image, which is per-GB rent for as long as it exists",
        "delete: destroy a server, image, volume or firewall",
        "dispatch: start a hosted CI run, which consumes a metered minute allowance"
      ],
      "runsAs": "the read-write credential, which exists on no node today",
      "cadence": "never on a timer, never as a retry, never as an escalation from a refused read"
    },
    "grant": {
      "requiredFields": [
        "scope",
        "budgetEur",
        "expiresAt",
        "issuedAt",
        "issuedBy",
        "nonce",
        "reason"
      ],
      "singleUse": true,
      "maxTtlHours": 24,
      "dir": "/var/lib/jarvisos/secrets/burst-grants",
      "storedAt": "One file per grant, <nonce>.json under the directory above, mode 0600, owned by the jarvis service account. Spent grants are moved to spent/ rather than deleted, so the ledger of who authorised what survives the run.",
      "scopeRule": "scope is an explicit list of operation ids. A wildcard is refused: '*' authorises every operation the adapter has, which is the opposite of an authorisation.",
      "teardownRule": "One grant covers a create and its own teardown. The reaper may delete only resources labelled with that grant's nonce, and may delete nothing else — otherwise a lease that has to wake a human to be enforced is not a lease.",
      "issuedBy": "The owner, at a terminal, running `node scripts/jarvisos-burst.mjs authorize`. JarvisOS cannot issue a grant to itself; the runner has no verb that writes one.",
      "refusals": [
        "An expired grant is not a grant.",
        "A replayed nonce is not a grant.",
        "A grant whose ceiling exceeds the policy's per-run ceiling is not a grant.",
        "A grant that does not name the operation is not a grant.",
        "A refused operation is refused. It is never retried with a stronger credential."
      ]
    }
  },
  "summary": {
    "targets": 6,
    "elastic": 3,
    "adopted": 2,
    "standby": 3,
    "rejected": 1,
    "workloads": 5,
    "monthlyHours": 133.5,
    "projectedMonthly": 5.24,
    "recurringMonthly": 5,
    "burstMonthly": 0.24,
    "ceilingMonthly": 10,
    "operations": 17,
    "autoOperations": 9,
    "ownerOperations": 8,
    "credentialsPresent": 0,
    "credentialsTotal": 4,
    "breakEvenExceeded": 0,
    "findings": 17,
    "crit": 2
  },
  "targets": [
    {
      "id": "vin-current",
      "name": "VIN as it is",
      "kind": "permanent",
      "decision": "adopt",
      "rejectedBecause": null,
      "provider": "hetzner",
      "entityId": "vin",
      "entityName": "VIN / VPS",
      "health": null,
      "basis": "measured",
      "summary": "The rented edge node, unchanged. Hetzner vServer in fsn1-dc14, 1 vCPU AMD EPYC-Genoa, 1.9 GiB, 38 GiB root. Public ingress, TLS and forward-auth live here and are not moving. It is the baseline every other row is compared against, not a burst target.",
      "measured": {
        "takenOn": "2026-08-17",
        "hostname": "ubuntu-2gb-fsn1-1",
        "vendor": "Hetzner (DMI sys_vendor), vServer, KVM",
        "region": "eu-central, availability zone fsn1-dc14",
        "cpu": "1 vCPU, AMD EPYC-Genoa",
        "memory": "1909 MiB total, 984 MiB available, 527 MiB of 2047 MiB swap in use",
        "disk": "38 GiB, 8.9 GiB used (25%)",
        "load": "0.04 0.03 0.00 at 7 days 9h uptime",
        "egressSinceBoot": "2.75 GB transmitted on eth0 over 7d 9h, about 11 GB/month"
      },
      "capacity": {
        "vcpu": 1,
        "ramGb": 1.9,
        "diskGb": 38
      },
      "cost": {
        "monthlyEur": 5,
        "basis": "unverified"
      },
      "startup": {
        "p50Sec": 0,
        "p95Sec": 0,
        "basis": "measured",
        "note": "Always on. That is the point and the problem."
      },
      "control": {
        "api": [
          "inventory",
          "resize",
          "rebuild",
          "destroy"
        ],
        "mechanism": "Hetzner Cloud API, once a credential exists"
      },
      "snapshots": {
        "mode": "api",
        "eurPerGbMonth": 0.0119,
        "typicalGb": 9,
        "expiryDays": 30
      },
      "network": {
        "tailscale": "native",
        "publicIngress": true,
        "metered": true,
        "includedEgressTb": 20
      },
      "state": "durable",
      "maxLeaseMinutes": null,
      "teardown": null,
      "billsWhenStopped": null,
      "monthlyCeilingEur": null,
      "reaper": null,
      "reaperInstalled": false,
      "hours": 0,
      "scoreHours": 20,
      "monthly": 5,
      "scores": {
        "cost": 2,
        "startup": 4,
        "control": 3,
        "snapshots": 3,
        "bandwidth": 4,
        "mesh": 4
      },
      "score": 3.15,
      "note": "The only target in this register with public ingress, and the only one allowed it. CPU is idle at load 0.04 while 527 MiB of swap is in use: this box is not short of compute, it is short of memory, which is why adding compute to it is the wrong purchase."
    },
    {
      "id": "vin-upgraded",
      "name": "VIN upgraded to CPX31",
      "kind": "permanent",
      "decision": "reject",
      "rejectedBecause": "It buys 720 hours a month of capacity to serve roughly 34 hours of transient work, and it buys them every month forever. At the projected utilisation the same work costs under a euro on demand. It also fails to fix the thing that is actually wrong: the memory pressure on VIN is caused by workloads the placement plan already moves to home1, so the upgrade would pay to keep them in the wrong place.",
      "provider": "hetzner",
      "entityId": null,
      "entityName": null,
      "health": null,
      "basis": "vendor-published",
      "summary": "The obvious answer: resize the existing VPS to 4 shared AMD vCPU and 8 GB. Kept in the register because a rejected option that disappears stops being evidence that a choice was made.",
      "measured": null,
      "capacity": {
        "vcpu": 4,
        "ramGb": 8,
        "diskGb": 160
      },
      "cost": {
        "monthlyEur": 15.59,
        "basis": "unverified",
        "note": "CPX31 list price plus the primary IPv4. Delta over vin-current is about EUR 10.59/month, which matches the EUR 10.50 avoided increase already recorded in config/placement.json."
      },
      "startup": {
        "p50Sec": 0,
        "p95Sec": 0,
        "basis": "vendor-published",
        "note": "Always on, but the resize itself needs a reboot of the public edge node — see rollback."
      },
      "control": {
        "api": [
          "inventory",
          "resize",
          "rebuild",
          "destroy"
        ],
        "mechanism": "Hetzner Cloud API change_type"
      },
      "snapshots": {
        "mode": "api",
        "eurPerGbMonth": 0.0119,
        "typicalGb": 12,
        "expiryDays": 30
      },
      "network": {
        "tailscale": "native",
        "publicIngress": true,
        "metered": true,
        "includedEgressTb": 20
      },
      "state": "durable",
      "maxLeaseMinutes": null,
      "teardown": null,
      "billsWhenStopped": null,
      "monthlyCeilingEur": null,
      "reaper": null,
      "reaperInstalled": false,
      "hours": 0,
      "scoreHours": 20,
      "monthly": 15.59,
      "scores": {
        "cost": 0,
        "startup": 4,
        "control": 3,
        "snapshots": 3,
        "bandwidth": 4,
        "mesh": 4
      },
      "score": 2.5500000000000003,
      "note": "A resize is not free of risk either: change_type on the public edge node reboots it, which is a TLS and forward-auth outage for every published hostname. The elastic options never touch the edge."
    },
    {
      "id": "home1-burst",
      "name": "Home1 scratch worker",
      "kind": "owned",
      "decision": "adopt",
      "rejectedBecause": null,
      "provider": "self",
      "entityId": "home1",
      "entityName": "Home1",
      "health": null,
      "basis": "estimated",
      "summary": "A container on the always-on home node, started per job and removed after it. Zero cash, already on the tailnet, and the target the placement plan already sends the agent fleet to. The default for everything transient.",
      "measured": null,
      "capacity": {
        "vcpu": 8,
        "ramGb": 32,
        "diskGb": 500
      },
      "cost": {
        "monthlyEur": 0,
        "hourlyEur": 0,
        "basis": "measured",
        "note": "Zero CASH. Not zero: config/placement.json records about EUR 4.50/month of electricity for home1, which the estate pays whether or not a build runs. Marginal cost of one more build is the difference in draw, which is far below the resolution of anything measurable here."
      },
      "startup": {
        "p50Sec": 4,
        "p95Sec": 20,
        "basis": "estimated",
        "coldExtraSec": 0,
        "note": "Container start on a warm host with the image already pulled. UNVERIFIED — home1 was not reachable from this sandbox, see gap burst-home1-unverified."
      },
      "control": {
        "api": [
          "create",
          "destroy",
          "inventory"
        ],
        "mechanism": "The existing JarvisOS dispatch contract in config/remote.json. Resize and rebuild are not verbs a physical machine has, which is why this row scores lower on controllability than a cloud VM and is still the right default."
      },
      "snapshots": {
        "mode": "manual",
        "eurPerGbMonth": 0,
        "typicalGb": 0,
        "expiryDays": 0
      },
      "network": {
        "tailscale": "native",
        "publicIngress": false,
        "metered": false,
        "includedEgressTb": 0
      },
      "state": "ephemeral",
      "maxLeaseMinutes": null,
      "teardown": null,
      "billsWhenStopped": null,
      "monthlyCeilingEur": null,
      "reaper": null,
      "reaperInstalled": false,
      "hours": 108,
      "scoreHours": 108,
      "monthly": 0,
      "scores": {
        "cost": 4,
        "startup": 3,
        "control": 2,
        "snapshots": 2,
        "bandwidth": 4,
        "mesh": 4
      },
      "score": 3.3000000000000003,
      "note": "Confirmed on the tailnet on 2026-08-17 as uk-james-home1 (100.96.106.91), idle, offering an exit node. The capacity figures are DECLARED, not measured — HOST-001 gap G1 is still open and this register inherits it."
    },
    {
      "id": "hetzner-cpx41",
      "name": "Hetzner CPX41 burst node",
      "kind": "elastic",
      "decision": "standby",
      "rejectedBecause": null,
      "provider": "hetzner-cloud",
      "entityId": null,
      "entityName": null,
      "health": null,
      "basis": "vendor-published",
      "summary": "8 dedicated-feel shared AMD vCPU and 16 GB, created on demand in a burst-only project, attached to the tailnet by cloud-init, destroyed when the job ends or the lease expires. Provisioned with no primary IPv4 at all: it is reachable over IPv6 and the tailnet, and from nowhere else.",
      "measured": null,
      "capacity": {
        "vcpu": 8,
        "ramGb": 16,
        "diskGb": 240
      },
      "cost": {
        "hourlyEur": 0.0433,
        "monthlyCapEur": 25.99,
        "fixedMonthlyEur": 0,
        "basis": "unverified",
        "note": "Hourly billing with the usual monthly cap. The cap is what bounds the damage when the reaper fails: worst case a forgotten node costs the cap, not the hourly rate multiplied by however long nobody looked. Provisioning without a primary IPv4 also removes about EUR 0.60/month per address and, more usefully, removes the address."
      },
      "startup": {
        "p50Sec": 35,
        "p95Sec": 75,
        "basis": "estimated",
        "coldExtraSec": 45,
        "note": "API create to SSH-ready is the vendor-typical 20-40s; cloud-init then installs and starts tailscaled. The 45s of coldExtraSec is the package install a kept snapshot would skip — and per burst-snapshot-is-recurring, that saving is rented monthly."
      },
      "control": {
        "api": [
          "create",
          "destroy",
          "resize",
          "rebuild",
          "inventory"
        ],
        "mechanism": "Hetzner Cloud API v1. The whole lifecycle is drivable, which is the axis this option wins on."
      },
      "snapshots": {
        "mode": "api",
        "eurPerGbMonth": 0.0119,
        "typicalGb": 12,
        "expiryDays": 30
      },
      "network": {
        "tailscale": "cloud-init",
        "publicIngress": false,
        "metered": true,
        "includedEgressTb": 20
      },
      "state": "ephemeral",
      "maxLeaseMinutes": 240,
      "teardown": "destroy",
      "billsWhenStopped": true,
      "monthlyCeilingEur": 8,
      "reaper": "scripts/jarvisos-burst.mjs reap, run on VIN's timer — never on the burst node itself, which cannot be trusted to delete the machine it is running on. Deletes only servers labelled with the nonce of the grant that created them.",
      "reaperInstalled": false,
      "hours": 5.5,
      "scoreHours": 5.5,
      "monthly": 0.23815,
      "scores": {
        "cost": 3,
        "startup": 2,
        "control": 4,
        "snapshots": 3,
        "bandwidth": 4,
        "mesh": 3
      },
      "score": 3.05,
      "note": "IPv6-only provisioning is not a compromise here: the metadata service on VIN already reports Hetzner's DNS64 resolvers (2a01:4ff:ff00::add:1 and ::add:2), so an IPv6-only node reaches IPv4-only package mirrors and the Tailscale coordination server through the provider's NAT64. Measured on VIN 2026-08-17."
    },
    {
      "id": "hetzner-cax31",
      "name": "Hetzner CAX31 burst node (ARM)",
      "kind": "elastic",
      "decision": "standby",
      "rejectedBecause": null,
      "provider": "hetzner-cloud",
      "entityId": null,
      "entityName": null,
      "health": null,
      "basis": "vendor-published",
      "summary": "8 Ampere ARM cores and 16 GB for roughly a third of the x86 hourly rate. The cheapest real compute in this register and the one with a sharp edge: it cannot build x86 container images, which is most of what this estate builds.",
      "measured": null,
      "capacity": {
        "vcpu": 8,
        "ramGb": 16,
        "diskGb": 160
      },
      "cost": {
        "hourlyEur": 0.0129,
        "monthlyCapEur": 7.49,
        "fixedMonthlyEur": 0,
        "basis": "unverified"
      },
      "startup": {
        "p50Sec": 35,
        "p95Sec": 75,
        "basis": "estimated",
        "coldExtraSec": 45
      },
      "control": {
        "api": [
          "create",
          "destroy",
          "resize",
          "rebuild",
          "inventory"
        ],
        "mechanism": "Hetzner Cloud API v1, identical to the x86 row — the adapter is parameterised by server type, not duplicated per architecture."
      },
      "snapshots": {
        "mode": "api",
        "eurPerGbMonth": 0.0119,
        "typicalGb": 10,
        "expiryDays": 30
      },
      "network": {
        "tailscale": "cloud-init",
        "publicIngress": false,
        "metered": true,
        "includedEgressTb": 20
      },
      "state": "ephemeral",
      "maxLeaseMinutes": 240,
      "teardown": "destroy",
      "billsWhenStopped": true,
      "monthlyCeilingEur": 4,
      "reaper": "scripts/jarvisos-burst.mjs reap, same timer and same nonce-labelled scope as the x86 row.",
      "reaperInstalled": false,
      "hours": 0,
      "scoreHours": 20,
      "monthly": 0,
      "scores": {
        "cost": 3,
        "startup": 2,
        "control": 4,
        "snapshots": 3,
        "bandwidth": 4,
        "mesh": 3
      },
      "score": 3.05,
      "note": "Eligible only for architecture-portable work. Benchmarks taken here are not comparable with numbers from VIN or home1, both of which are x86 — which makes it the wrong target for the one workload that would most like the price. See gap burst-arm-not-x86."
    },
    {
      "id": "github-actions",
      "name": "GitHub Actions hosted runner",
      "kind": "managed",
      "decision": "standby",
      "rejectedBecause": null,
      "provider": "github",
      "entityId": null,
      "entityName": null,
      "health": null,
      "basis": "vendor-published",
      "summary": "Someone else's ephemeral VM, billed by the minute against a free monthly allowance, with no machine to forget about. The right answer for repository CI and the wrong one for anything that needs to reach the estate.",
      "measured": null,
      "capacity": {
        "vcpu": 4,
        "ramGb": 16,
        "diskGb": 14
      },
      "cost": {
        "hourlyEur": 0.44,
        "includedHoursPerMonth": 33,
        "fixedMonthlyEur": 0,
        "basis": "unverified",
        "note": "2000 free minutes a month on the free plan for private repositories, unlimited for public ones; the hourly figure is the standard 2-core Linux per-minute rate beyond the allowance. Over the allowance it is by far the most expensive compute in this register — ten times the CPX41 hourly rate — which is exactly why the allowance is watched by an automatic read."
      },
      "startup": {
        "p50Sec": 20,
        "p95Sec": 90,
        "basis": "estimated",
        "coldExtraSec": 0,
        "note": "Queue time, not boot time, and it is not under this estate's control."
      },
      "control": {
        "api": [
          "create",
          "inventory"
        ],
        "mechanism": "workflow_dispatch and the Actions REST API. There is no resize, no rebuild and no delete: the runner is not a machine this estate owns, which is simultaneously the reason it is safe and the reason it is uncontrollable."
      },
      "snapshots": {
        "mode": "none",
        "eurPerGbMonth": 0,
        "typicalGb": 0,
        "expiryDays": 0
      },
      "network": {
        "tailscale": "cloud-init",
        "publicIngress": false,
        "metered": false,
        "includedEgressTb": 0
      },
      "state": "ephemeral",
      "maxLeaseMinutes": 240,
      "teardown": "destroy",
      "billsWhenStopped": false,
      "monthlyCeilingEur": 0,
      "reaper": "The platform's own job timeout, pinned lower by timeout-minutes on every workflow this estate dispatches. Nothing survives a run, so there is nothing for a reaper to sweep.",
      "reaperInstalled": false,
      "hours": 20,
      "scoreHours": 20,
      "monthly": 0,
      "scores": {
        "cost": 4,
        "startup": 2,
        "control": 2,
        "snapshots": 0,
        "bandwidth": 4,
        "mesh": 3
      },
      "score": 2.75,
      "note": "If a job here is given a tailnet node it must be an ephemeral, tagged, pre-approved one that expires with the run. A long-lived tailnet auth key in a CI secret is a permanent door into the estate held by a vendor."
    }
  ],
  "workloads": [
    {
      "id": "agent-worker",
      "name": "Coding agent worker",
      "summary": "A `claude -p` worker dispatched by the control plane against one repository. Memory-hungry, CPU-light, and the largest single consumer of VIN's memory today.",
      "state": "ephemeral",
      "dataSensitivity": "secret",
      "profile": {
        "vcpu": 2,
        "ramGb": 2,
        "diskGb": 10,
        "durationMinutes": 25,
        "runsPerMonth": 120,
        "parallelism": 2
      },
      "hours": 100,
      "eligibleTargets": [
        "home1-burst",
        "hetzner-cpx41"
      ],
      "preferredTarget": "home1-burst",
      "recommendedTarget": "home1-burst",
      "reason": "Measured at 366 and 337 MiB resident on VIN on 2026-08-16, about 40% of that node's total memory. The placement plan already moves this class to home1, and it needs a repository checkout and a model API key — material that should not be created and destroyed inside a rented block device.",
      "paidBecause": null,
      "paidEvidence": null,
      "evidence": "docs/HOSTING-TOPOLOGY.md section 2; ps aux on VIN 2026-08-16",
      "options": [
        {
          "id": "home1-burst",
          "name": "Home1 scratch worker",
          "kind": "owned",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 20,
          "scores": {
            "cost": 4,
            "startup": 3,
            "control": 2,
            "snapshots": 2,
            "bandwidth": 4,
            "mesh": 4
          },
          "score": 3.3000000000000003
        },
        {
          "id": "hetzner-cpx41",
          "name": "Hetzner CPX41 burst node",
          "kind": "elastic",
          "fits": true,
          "monthly": 4.33,
          "perRunEur": 0.018041666666666668,
          "startupSec": 75,
          "scores": {
            "cost": 2,
            "startup": 2,
            "control": 4,
            "snapshots": 3,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 2.75
        }
      ]
    },
    {
      "id": "container-build",
      "name": "x86 container image build",
      "summary": "Building and pushing an image the estate will actually run. Bursty CPU, large disk churn, no durable state of its own.",
      "state": "ephemeral",
      "dataSensitivity": "internal",
      "profile": {
        "vcpu": 4,
        "ramGb": 8,
        "diskGb": 40,
        "durationMinutes": 12,
        "runsPerMonth": 40,
        "parallelism": 1
      },
      "hours": 8,
      "eligibleTargets": [
        "home1-burst",
        "hetzner-cpx41",
        "github-actions"
      ],
      "preferredTarget": "home1-burst",
      "recommendedTarget": "home1-burst",
      "reason": "Eight hours a month. It fits inside home1 comfortably and the images are pushed onto the tailnet anyway, so building them there avoids a WAN round trip of the whole layer set. CPX41 is the overflow when home1 is busy or down; the ARM node is deliberately not eligible because the artefact is x86.",
      "paidBecause": null,
      "paidEvidence": null,
      "evidence": "config/placement.json workloads; the build is the same one npm run build performs",
      "options": [
        {
          "id": "home1-burst",
          "name": "Home1 scratch worker",
          "kind": "owned",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 20,
          "scores": {
            "cost": 4,
            "startup": 3,
            "control": 2,
            "snapshots": 2,
            "bandwidth": 4,
            "mesh": 4
          },
          "score": 3.3000000000000003
        },
        {
          "id": "hetzner-cpx41",
          "name": "Hetzner CPX41 burst node",
          "kind": "elastic",
          "fits": true,
          "monthly": 0.3464,
          "perRunEur": 0.00866,
          "startupSec": 75,
          "scores": {
            "cost": 3,
            "startup": 2,
            "control": 4,
            "snapshots": 3,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 3.05
        },
        {
          "id": "github-actions",
          "name": "GitHub Actions hosted runner",
          "kind": "managed",
          "fits": false,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 90,
          "scores": {
            "cost": 4,
            "startup": 2,
            "control": 2,
            "snapshots": 0,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 2.75
        }
      ]
    },
    {
      "id": "benchmark",
      "name": "Repeatable benchmark run",
      "summary": "A timing run whose whole value is that the numbers are comparable between runs. Needs cores nobody else is using for the length of the run.",
      "state": "ephemeral",
      "dataSensitivity": "public",
      "profile": {
        "vcpu": 8,
        "ramGb": 16,
        "diskGb": 40,
        "durationMinutes": 30,
        "runsPerMonth": 8,
        "parallelism": 1
      },
      "hours": 4,
      "eligibleTargets": [
        "home1-burst",
        "hetzner-cpx41"
      ],
      "preferredTarget": "hetzner-cpx41",
      "recommendedTarget": "home1-burst",
      "reason": "The one class in this register where paid capacity removes a measured operational problem. It is also the cheapest line in it.",
      "paidBecause": "A benchmark sharing cores with the agent fleet, the control plane and the databases measures the noise, not the change. Home1 is the estate's default compute node precisely because it is busy; four hours a month of a machine with nothing else on it is the only way these numbers mean anything. The alternative interpretation — that the numbers do not need to be comparable — makes the workload pointless rather than cheap.",
      "paidEvidence": "VIN's own load average of 0.04 alongside 527 MiB of swap in use is the shape of the problem: contention on this estate is invisible in CPU and lethal in memory, so a benchmark on a shared node is unreproducible in a way that does not show up in the load average. Cost of the fix at the declared rate: 4 h/month at EUR 0.0433/h, about EUR 0.17/month, against EUR 10.59/month for the permanent upgrade that would still be contended and would still only be 4 vCPU.",
      "evidence": "free -m and uptime on VIN 2026-08-17",
      "options": [
        {
          "id": "home1-burst",
          "name": "Home1 scratch worker",
          "kind": "owned",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 20,
          "scores": {
            "cost": 4,
            "startup": 3,
            "control": 2,
            "snapshots": 2,
            "bandwidth": 4,
            "mesh": 4
          },
          "score": 3.3000000000000003
        },
        {
          "id": "hetzner-cpx41",
          "name": "Hetzner CPX41 burst node",
          "kind": "elastic",
          "fits": true,
          "monthly": 0.1732,
          "perRunEur": 0.02165,
          "startupSec": 75,
          "scores": {
            "cost": 3,
            "startup": 2,
            "control": 4,
            "snapshots": 3,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 3.05
        }
      ]
    },
    {
      "id": "ci-test",
      "name": "Repository test suite",
      "summary": "npm test and npm run check on every push. Short, frequent, and entirely self-contained: it needs a checkout and Node, and nothing from the estate.",
      "state": "ephemeral",
      "dataSensitivity": "internal",
      "profile": {
        "vcpu": 2,
        "ramGb": 4,
        "diskGb": 10,
        "durationMinutes": 6,
        "runsPerMonth": 200,
        "parallelism": 1
      },
      "hours": 20,
      "eligibleTargets": [
        "home1-burst",
        "github-actions",
        "hetzner-cax31"
      ],
      "preferredTarget": "github-actions",
      "recommendedTarget": "home1-burst",
      "reason": "Twenty hours a month, which is 1200 minutes against a 2000-minute free allowance. It needs no tailnet access at all — this repository has zero runtime dependencies and the suite is pure Node — so it is the one class that can safely run on hardware the estate does not control. Home1 is the fallback the moment the allowance is short, and the ARM node is eligible because the test suite is architecture-portable even though the images are not.",
      "paidBecause": null,
      "paidEvidence": null,
      "evidence": "package.json test script; 57k lines of source with no dependencies block",
      "options": [
        {
          "id": "home1-burst",
          "name": "Home1 scratch worker",
          "kind": "owned",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 20,
          "scores": {
            "cost": 4,
            "startup": 3,
            "control": 2,
            "snapshots": 2,
            "bandwidth": 4,
            "mesh": 4
          },
          "score": 3.3000000000000003
        },
        {
          "id": "hetzner-cax31",
          "name": "Hetzner CAX31 burst node (ARM)",
          "kind": "elastic",
          "fits": true,
          "monthly": 0.258,
          "perRunEur": 0.0012900000000000001,
          "startupSec": 75,
          "scores": {
            "cost": 3,
            "startup": 2,
            "control": 4,
            "snapshots": 3,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 3.05
        },
        {
          "id": "github-actions",
          "name": "GitHub Actions hosted runner",
          "kind": "managed",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 90,
          "scores": {
            "cost": 4,
            "startup": 2,
            "control": 2,
            "snapshots": 0,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 2.75
        }
      ]
    },
    {
      "id": "restore-drill",
      "name": "Offsite restore rehearsal",
      "summary": "Pull the encrypted offsite copy onto a machine that has never seen it, decrypt it, and prove it restores. Large disk, long run, once a month.",
      "state": "ephemeral",
      "dataSensitivity": "secret",
      "profile": {
        "vcpu": 2,
        "ramGb": 4,
        "diskGb": 200,
        "durationMinutes": 90,
        "runsPerMonth": 1
      },
      "hours": 1.5,
      "eligibleTargets": [
        "home1-burst",
        "hetzner-cpx41"
      ],
      "preferredTarget": "hetzner-cpx41",
      "recommendedTarget": "home1-burst",
      "reason": "Eligible on home1 as the free fallback, and the fallback is genuinely worse rather than merely cheaper — which is what a fallback is supposed to be.",
      "paidBecause": "A restore drill run on home1 rehearses the case where home1 still exists, which is the case that does not need the offsite copy. Restoring onto a machine in a different failure domain, from nothing but the bucket and the passphrase, is the only version of this drill that proves anything. One and a half hours a month.",
      "paidEvidence": "config/placement.json failure domains: home1 and the NAS are both fd-home-rack, and the offsite copy in fd-offsite exists specifically to survive that domain. A drill inside the domain it is protecting against is a test with the failure removed. Cost at the declared rate: 1.5 h/month at EUR 0.0433/h, about EUR 0.07/month.",
      "evidence": "docs/DATA-RESILIENCE.md; config/placement.json backupTiers",
      "options": [
        {
          "id": "home1-burst",
          "name": "Home1 scratch worker",
          "kind": "owned",
          "fits": true,
          "monthly": 0,
          "perRunEur": 0,
          "startupSec": 20,
          "scores": {
            "cost": 4,
            "startup": 3,
            "control": 2,
            "snapshots": 2,
            "bandwidth": 4,
            "mesh": 4
          },
          "score": 3.3000000000000003
        },
        {
          "id": "hetzner-cpx41",
          "name": "Hetzner CPX41 burst node",
          "kind": "elastic",
          "fits": true,
          "monthly": 0.06495,
          "perRunEur": 0.06495,
          "startupSec": 75,
          "scores": {
            "cost": 3,
            "startup": 2,
            "control": 4,
            "snapshots": 3,
            "bandwidth": 4,
            "mesh": 3
          },
          "score": 3.05
        }
      ]
    }
  ],
  "breakEven": [
    {
      "elasticId": "hetzner-cpx41",
      "elasticName": "Hetzner CPX41 burst node",
      "permanentId": "vin-current",
      "permanentName": "VIN as it is",
      "hours": 115.47344110854505,
      "projectedHours": 5.5,
      "elasticMonthly": 0.23815,
      "permanentMonthly": 5,
      "exceeded": false
    },
    {
      "elasticId": "hetzner-cpx41",
      "elasticName": "Hetzner CPX41 burst node",
      "permanentId": "vin-upgraded",
      "permanentName": "VIN upgraded to CPX31",
      "hours": 360.0461893764434,
      "projectedHours": 5.5,
      "elasticMonthly": 0.23815,
      "permanentMonthly": 15.59,
      "exceeded": false
    },
    {
      "elasticId": "hetzner-cax31",
      "elasticName": "Hetzner CAX31 burst node (ARM)",
      "permanentId": "vin-current",
      "permanentName": "VIN as it is",
      "hours": 387.5968992248062,
      "projectedHours": 0,
      "elasticMonthly": 0,
      "permanentMonthly": 5,
      "exceeded": false
    },
    {
      "elasticId": "hetzner-cax31",
      "elasticName": "Hetzner CAX31 burst node (ARM)",
      "permanentId": "vin-upgraded",
      "permanentName": "VIN upgraded to CPX31",
      "hours": null,
      "projectedHours": 0,
      "elasticMonthly": 0,
      "permanentMonthly": 15.59,
      "exceeded": false
    },
    {
      "elasticId": "github-actions",
      "elasticName": "GitHub Actions hosted runner",
      "permanentId": "vin-current",
      "permanentName": "VIN as it is",
      "hours": 44.36363636363636,
      "projectedHours": 20,
      "elasticMonthly": 0,
      "permanentMonthly": 5,
      "exceeded": false
    },
    {
      "elasticId": "github-actions",
      "elasticName": "GitHub Actions hosted runner",
      "permanentId": "vin-upgraded",
      "permanentName": "VIN upgraded to CPX31",
      "hours": 68.43181818181819,
      "projectedHours": 20,
      "elasticMonthly": 0,
      "permanentMonthly": 15.59,
      "exceeded": false
    }
  ],
  "operations": [
    {
      "id": "hz-pricing",
      "provider": "hetzner-cloud",
      "summary": "The provider's own price list. Every euro in this register is arithmetic on a number that this call is how you check.",
      "method": "GET",
      "path": "/v1/pricing",
      "params": [],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-server-types",
      "provider": "hetzner-cloud",
      "summary": "Available server types with their per-hour and per-month prices, used to resolve a workload profile to a concrete type.",
      "method": "GET",
      "path": "/v1/server_types",
      "params": [],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-servers-list",
      "provider": "hetzner-cloud",
      "summary": "Inventory. What exists in the burst project right now, with labels. The call the reaper decides on and the call that answers 'is anything running that nobody remembers'.",
      "method": "GET",
      "path": "/v1/servers",
      "params": [
        "label_selector"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-server-get",
      "provider": "hetzner-cloud",
      "summary": "Health of one burst node: power state, creation time and therefore its age against the lease.",
      "method": "GET",
      "path": "/v1/servers/{id}",
      "params": [
        "id"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-server-metrics",
      "provider": "hetzner-cloud",
      "summary": "CPU, disk and network series for a running burst node. Read-only and free, so a stuck job is visible without touching it.",
      "method": "GET",
      "path": "/v1/servers/{id}/metrics",
      "params": [
        "id",
        "type",
        "start",
        "end"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-images-list",
      "provider": "hetzner-cloud",
      "summary": "Which snapshots exist and how large they are, which is the same question as how much per-GB rent is being paid this month.",
      "method": "GET",
      "path": "/v1/images",
      "params": [
        "type",
        "label_selector"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-firewalls-list",
      "provider": "hetzner-cloud",
      "summary": "The deny-all-inbound firewall every burst node is created with. Read so the console can say whether it still exists before anything is provisioned into it.",
      "method": "GET",
      "path": "/v1/firewalls",
      "params": [],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "hz-server-create",
      "provider": "hetzner-cloud",
      "summary": "Create a burst node: server type from the workload profile, no primary IPv4, deny-all-inbound firewall, cloud-init that joins the tailnet with an ephemeral pre-authorised key, and labels carrying the grant nonce and the lease deadline.",
      "method": "POST",
      "path": "/v1/servers",
      "params": [
        "name",
        "server_type",
        "image",
        "location",
        "firewalls",
        "labels",
        "user_data",
        "public_net"
      ],
      "effect": "create",
      "approval": "owner",
      "billable": true,
      "confirm": true,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": null,
      "note": "public_net must set enable_ipv4 false. A burst node with a public IPv4 is a new internet-facing host in an estate that has exactly one, and it costs extra for the privilege."
    },
    {
      "id": "hz-server-delete",
      "provider": "hetzner-cloud",
      "summary": "Destroy a burst node. The only teardown that stops the invoice, and therefore the only teardown this contract recognises.",
      "method": "DELETE",
      "path": "/v1/servers/{id}",
      "params": [
        "id"
      ],
      "effect": "destroy",
      "approval": "owner",
      "billable": true,
      "confirm": true,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": null,
      "note": "Carried by the same grant that authorised the create, restricted to servers labelled with that grant's nonce. Billable is true because a delete is a chargeable state change on a metered resource, and because marking it free would let it slip out of the owner band."
    },
    {
      "id": "hz-server-poweroff",
      "provider": "hetzner-cloud",
      "summary": "Power a burst node off without deleting it. Present in the adapter so the contract can say what it is for, which is debugging a failed run before destroying the evidence.",
      "method": "POST",
      "path": "/v1/servers/{id}/actions/poweroff",
      "params": [
        "id"
      ],
      "effect": "mutate",
      "approval": "owner",
      "billable": true,
      "confirm": false,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": "THIS DOES NOT STOP BILLING. A powered-off server bills at the full rate until it is deleted. It is not teardown and this register refuses to let any target declare it as such.",
      "note": null
    },
    {
      "id": "hz-server-resize",
      "provider": "hetzner-cloud",
      "summary": "Change a server type, in either direction. Never used on a burst node — a burst node is destroyed and recreated — and present only because resizing VIN is the rejected option this register is compared against.",
      "method": "POST",
      "path": "/v1/servers/{id}/actions/change_type",
      "params": [
        "id",
        "server_type",
        "upgrade_disk"
      ],
      "effect": "mutate",
      "approval": "owner",
      "billable": true,
      "confirm": false,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": "Reboots the server. On VIN that is a TLS and forward-auth outage for every published hostname, and upgrade_disk is irreversible: a disk that grew cannot shrink back to the smaller tier.",
      "note": null
    },
    {
      "id": "hz-server-rebuild",
      "provider": "hetzner-cloud",
      "summary": "Replace a running server's image. Not billable — it changes no tier — and still owner-gated, because destroying a filesystem is destructive whether or not it costs anything.",
      "method": "POST",
      "path": "/v1/servers/{id}/actions/rebuild",
      "params": [
        "id",
        "image"
      ],
      "effect": "mutate",
      "approval": "owner",
      "billable": false,
      "confirm": false,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": "Erases the root filesystem. This is the operation that proves billable and dangerous are two different axes.",
      "note": null
    },
    {
      "id": "hz-image-create",
      "provider": "hetzner-cloud",
      "summary": "Snapshot a burst node to make the next boot faster. Creates a recurring per-GB charge that lasts until somebody deletes it.",
      "method": "POST",
      "path": "/v1/servers/{id}/actions/create_image",
      "params": [
        "id",
        "type",
        "description",
        "labels"
      ],
      "effect": "create",
      "approval": "owner",
      "billable": true,
      "confirm": false,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": "The only operation here that converts variable cost into recurring cost. Every snapshot carries an expiry label and the reaper enforces it.",
      "note": null
    },
    {
      "id": "hz-image-delete",
      "provider": "hetzner-cloud",
      "summary": "Delete a snapshot and stop paying rent on it.",
      "method": "DELETE",
      "path": "/v1/images/{id}",
      "params": [
        "id"
      ],
      "effect": "destroy",
      "approval": "owner",
      "billable": false,
      "confirm": true,
      "credentialId": "hetzner-write",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "gh-runs-list",
      "provider": "github",
      "summary": "Which dispatched CI runs exist and what happened to them.",
      "method": "GET",
      "path": "/repos/{owner}/{repo}/actions/runs",
      "params": [
        "owner",
        "repo",
        "status"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "github-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "gh-billing-actions",
      "provider": "github",
      "summary": "Minutes consumed against the free allowance this month. The automatic read that watches the ceiling, so the first sign of running over is not an invoice.",
      "method": "GET",
      "path": "/users/{user}/settings/billing/actions",
      "params": [
        "user"
      ],
      "effect": "read",
      "approval": "auto",
      "billable": false,
      "confirm": false,
      "credentialId": "github-read",
      "credentialPresent": false,
      "warning": null,
      "note": null
    },
    {
      "id": "gh-workflow-dispatch",
      "provider": "github",
      "summary": "Start a hosted CI run. Free inside the monthly allowance and roughly ten times the CPX41 hourly rate outside it, so it is metered work and therefore owner-gated.",
      "method": "POST",
      "path": "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches",
      "params": [
        "owner",
        "repo",
        "workflow_id",
        "ref",
        "inputs"
      ],
      "effect": "create",
      "approval": "owner",
      "billable": true,
      "confirm": true,
      "credentialId": "github-dispatch",
      "credentialPresent": false,
      "warning": null,
      "note": "The grant for this one carries the expected minute cost, not a euro cost, converted at the published rate. An allowance is a budget even when it is denominated in minutes."
    }
  ],
  "credentials": [
    {
      "id": "hetzner-read",
      "provider": "hetzner-cloud",
      "purpose": "Read-only inventory, health, pricing and image listing in the burst-only project. The credential every automatic operation uses.",
      "envName": "HCLOUD_TOKEN_READ",
      "location": "/var/lib/jarvisos/secrets/hetzner-read.token",
      "mode": "0600",
      "owner": "jarvis",
      "readOnly": true,
      "rotationDays": 180,
      "allows": [
        "list and inspect servers, server types, images, firewalls and locations in the burst project",
        "read server metrics",
        "read the price list"
      ],
      "forbids": [
        "anything that changes state",
        "any access to the project VIN lives in"
      ],
      "custody": "Minted by the owner in the Hetzner console against the burst-only project, written to the location above with mode 0600, never pasted into a task, a commit or a chat.",
      "present": false
    },
    {
      "id": "hetzner-write",
      "provider": "hetzner-cloud",
      "purpose": "Create and destroy burst nodes and their snapshots. The credential the owner authorisation policy exists to gate.",
      "envName": "HCLOUD_TOKEN_WRITE",
      "location": "/var/lib/jarvisos/secrets/hetzner-write.token",
      "mode": "0600",
      "owner": "jarvis",
      "readOnly": false,
      "rotationDays": 90,
      "allows": [
        "create a server in the burst project with a fixed firewall and no primary IPv4",
        "delete a server labelled with the nonce of the grant that created it",
        "create and delete snapshots in the burst project"
      ],
      "forbids": [
        "any access to the project VIN lives in — enforced by the provider, because a Hetzner token is scoped to one project",
        "resizing anything that is not a burst node",
        "existing on a burst node: the token stays on VIN and is never passed into user_data"
      ],
      "custody": "Does not exist yet. Minted only when the owner authorises the first run, and revoked in the provider console the moment the experiment ends.",
      "present": false
    },
    {
      "id": "github-read",
      "provider": "github",
      "purpose": "Read dispatched run status and the Actions minute allowance.",
      "envName": "GITHUB_TOKEN_BURST_READ",
      "location": "/var/lib/jarvisos/secrets/github-burst-read.token",
      "mode": "0600",
      "owner": "jarvis",
      "readOnly": true,
      "rotationDays": 180,
      "allows": [
        "read workflow runs",
        "read the Actions billing summary"
      ],
      "forbids": [
        "starting a run",
        "any write to any repository"
      ],
      "custody": "A fine-grained token limited to the repositories this estate dispatches into. Deliberately NOT the general-purpose PAT that already exists at /etc/jarvisos/github.pat, whose scope is unbounded relative to this contract.",
      "present": false
    },
    {
      "id": "github-dispatch",
      "provider": "github",
      "purpose": "Start a hosted CI run, spending metered minutes.",
      "envName": "GITHUB_TOKEN_BURST_DISPATCH",
      "location": "/var/lib/jarvisos/secrets/github-burst-dispatch.token",
      "mode": "0600",
      "owner": "jarvis",
      "readOnly": false,
      "rotationDays": 90,
      "allows": [
        "dispatch a named workflow on a named ref"
      ],
      "forbids": [
        "pushing code",
        "changing workflow definitions",
        "reading repository secrets"
      ],
      "custody": "Does not exist yet. A fine-grained token with actions:write on exactly the repositories that have a dispatchable workflow, and nothing else.",
      "present": false
    }
  ],
  "lifecycle": [
    {
      "id": "select",
      "action": "Resolve the workload class to a target: the highest weighted score among eligible targets that fit the profile, free targets first at equal score.",
      "approval": "auto",
      "note": "Pure arithmetic on this file. Costs nothing, changes nothing, and is the step that usually ends with 'home1'."
    },
    {
      "id": "estimate",
      "action": "Derive the per-run cost and the month-to-date spend against the ceiling.",
      "approval": "auto",
      "operation": "hz-pricing",
      "note": "Reads the provider's price list so the estimate is against today's prices rather than the ones written in this file."
    },
    {
      "id": "authorise",
      "action": "The owner issues a grant: scope, ceiling, expiry, nonce, reason. JarvisOS cannot issue one to itself.",
      "approval": "owner",
      "note": "The gate. Everything above this line is free and reversible; everything below it is neither."
    },
    {
      "id": "provision",
      "action": "Create the node with no public IPv4, a deny-all-inbound firewall, and labels carrying the grant nonce and the lease deadline.",
      "approval": "owner",
      "operation": "hz-server-create",
      "boundedBy": "The grant's ceiling and the target's monthlyCeilingEur, whichever is lower."
    },
    {
      "id": "attach",
      "action": "cloud-init installs tailscaled and joins the tailnet with an ephemeral, tagged, pre-authorised key that expires with the node.",
      "approval": "owner",
      "note": "Part of the create call's user_data, so it is the same authorisation. The key is ephemeral and tagged tag:burst: an ACL grants tag:burst only the ports a job needs, and a node that dies leaves no tailnet record behind."
    },
    {
      "id": "verify",
      "action": "Confirm the node is up, reachable over the tailnet and inside its lease before any work is sent to it.",
      "approval": "auto",
      "operation": "hz-server-get"
    },
    {
      "id": "run",
      "action": "Dispatch the job over the tailnet using the existing remote dispatch contract in config/remote.json.",
      "approval": "auto",
      "note": "Deliberately the same dispatch path as home1. A burst node is just another node with a shorter life; inventing a second way to run work on it would double the surface that has to be trusted."
    },
    {
      "id": "collect",
      "action": "Pull the evidence back before teardown. Anything not collected is gone, because the disk is about to be deleted.",
      "approval": "auto"
    },
    {
      "id": "destroy",
      "action": "Delete the node. Not stop, not power off — delete.",
      "approval": "owner",
      "operation": "hz-server-delete",
      "boundedBy": "Servers labelled with the nonce of the grant that created them, and nothing else."
    },
    {
      "id": "reap",
      "action": "Sweep for burst resources past their lease deadline and delete them, then sweep for snapshots past their expiry and delete those.",
      "approval": "owner",
      "operation": "hz-server-delete",
      "boundedBy": "Same nonce restriction. Carried by the grant that created each resource, which is what lets an unattended timer enforce a lease without holding a standing authority to delete.",
      "note": "Runs on VIN, never on a burst node. A machine cannot be trusted to delete itself, and the case the reaper exists for is precisely the one where the burst node is wedged."
    }
  ],
  "rollback": [
    {
      "scenario": "The elastic experiment is abandoned",
      "action": "Set decision to 'reject' on hetzner-cpx41, hetzner-cax31 and github-actions, and set every workload's preferredTarget to its free eligible target. Revoke both Hetzner tokens in the provider console and delete the burst project. npm run check enforces that a free fallback existed for every class, so this is an edit rather than a migration.",
      "owner": "operator",
      "reversible": true,
      "timeToRecover": "minutes"
    },
    {
      "scenario": "A burst node is wedged and the reaper did not delete it",
      "action": "node scripts/jarvisos-burst.mjs reap --confirm, or delete the server in the Hetzner console. The monthly cap bounds the damage in the meantime: worst case is the target's cap, not the hourly rate multiplied by however long nobody looked.",
      "owner": "operator",
      "reversible": true,
      "timeToRecover": "under an hour, bounded by the cap regardless"
    },
    {
      "scenario": "The write credential leaks",
      "action": "Revoke the token in the Hetzner console. Because it is scoped to the burst-only project, the blast radius is burst nodes and their snapshots — VIN, its volumes and its snapshots are in a different project and are not reachable with it. Delete the burst project outright if in doubt.",
      "owner": "owner",
      "reversible": true,
      "timeToRecover": "minutes, and this project separation is the reason it is minutes"
    },
    {
      "scenario": "A grant is issued in error",
      "action": "Delete the grant file from /var/lib/jarvisos/secrets/burst-grants/. It is single-use and expiring; an unspent grant that is deleted was never spent. If it was already spent, the resource it created carries its nonce and is deletable by that label.",
      "owner": "owner",
      "reversible": true,
      "timeToRecover": "immediate"
    },
    {
      "scenario": "The permanent upgrade turns out to have been right after all",
      "action": "The register says so before the invoice does: when projected hours cross the derived break-even, the console raises burst-exceeds-breakeven and names the permanent option and its price. Resize VIN with hz-server-resize, accepting the reboot of the public edge, or provision a second permanent node so the edge is never rebooted for a capacity decision.",
      "owner": "owner",
      "reversible": false,
      "timeToRecover": "a resize is a reboot; a disk that grew cannot shrink back"
    },
    {
      "scenario": "Tailscale attach fails on a new burst node",
      "action": "The node is unreachable and must be destroyed rather than debugged: it has no public IPv4 and no inbound firewall rule, which is the design working. Debug by provisioning with a serial console session in the provider UI, never by adding an address.",
      "owner": "operator",
      "reversible": true,
      "timeToRecover": "minutes"
    }
  ],
  "findings": [
    {
      "code": "burst-gap-reaper-has-no-timer",
      "severity": "crit",
      "title": "The reaper is written but nothing runs it on a schedule",
      "detail": "A lease is only a lease if something enforces it. Until a timer on VIN runs `jarvisos-burst.mjs reap`, the lease is a comment and the monthly cap is the only real bound. Installing that timer is part of enabling the first elastic target, not a follow-up.",
      "fatal": false,
      "scope": "operator"
    },
    {
      "code": "burst-gap-tailnet-acl-for-burst",
      "severity": "crit",
      "title": "No tailnet ACL for tag:burst exists yet",
      "detail": "The design has burst nodes joining with an ephemeral key tagged tag:burst, and an ACL granting that tag only the ports a job needs. Without the ACL an ephemeral node joins with whatever the tailnet's default policy allows, which on a flat tailnet is everything. Writing the ACL is an owner action in the Tailscale admin console.",
      "fatal": false,
      "scope": "owner"
    },
    {
      "code": "burst-secret-off-owned-hardware",
      "severity": "warn",
      "title": "Offsite restore rehearsal handles secret material on Hetzner CPX41 burst node",
      "detail": "A rented machine that is created, used and deleted leaves the material in somebody else's block store until it is overwritten. Keep this class on owned hardware, or state what is done about it.",
      "fatal": false,
      "scope": "restore-drill"
    },
    {
      "code": "burst-gap-prices-unverified",
      "severity": "warn",
      "title": "Every price in this register is unverified against an invoice or the live price list",
      "detail": "No provider credential exists on this node, so hz-pricing has never been called and the figures are from published list prices carried in this file. The arithmetic is sound; the inputs are assumptions. Verify before the first authorised run, not after the first bill.",
      "fatal": false,
      "scope": "owner"
    },
    {
      "code": "burst-gap-home1-unverified",
      "severity": "warn",
      "title": "Home1's capacity is declared, not measured, so the free default may not actually fit the workloads routed to it",
      "detail": "Inherited from HOST-001 gap G1: home1 was not reachable or enumerable from this sandbox. It is confirmed present on the tailnet as uk-james-home1 and idle, which is not the same as confirmed to have 8 cores and 32 GB. If it does not, the burst targets stop being overflow and start being load-bearing.",
      "fatal": false,
      "scope": "operator"
    },
    {
      "code": "burst-gap-adapter-never-executed",
      "severity": "warn",
      "title": "The provider adapter is a plan; not one operation in it has ever been run",
      "detail": "No Hetzner or GitHub burst credential exists in /var/lib/jarvisos/secrets. Startup latency, the cloud-init tailnet attach and the reaper's label filter are all estimated rather than measured. The first authorised run is also the first test.",
      "fatal": false,
      "scope": "operator"
    },
    {
      "code": "burst-gap-hetzner-token-scope-is-coarse",
      "severity": "warn",
      "title": "Hetzner API tokens are read or read-write for a whole project; there is no 'may create but not delete' scope",
      "detail": "The write credential is therefore broader than this contract wants. The mitigation is structural rather than token-based: burst resources live in their own Hetzner project containing nothing else, so the widest thing that token can do is destroy the burst project. VIN is in a different project and is not reachable with it. Creating that project is an owner action and has not been done.",
      "fatal": false,
      "scope": "owner"
    },
    {
      "code": "burst-gap-restore-drill-passphrase",
      "severity": "warn",
      "title": "The restore drill needs the offsite passphrase, which by policy is on no node",
      "detail": "config/placement.json requires the offsite encryption passphrase to live in offline custody only, and the placement validator enforces it. So the drill cannot be fully automatic: the owner supplies the passphrase into the burst node's session for the length of the run, and the node is destroyed afterwards. That is workable, and it is also why the drill class is marked secret and raises burst-secret-off-owned-hardware.",
      "fatal": false,
      "scope": "owner"
    },
    {
      "code": "burst-preference-contradicted",
      "severity": "note",
      "title": "Repository test suite prefers GitHub Actions hosted runner; the arithmetic prefers Home1 scratch worker",
      "detail": "Weighted 3.30 against 2.75 at 20.0 h/month. The preference may still be right — it can encode something the six axes do not — but it should be a decision rather than an oversight.",
      "fatal": false,
      "scope": "ci-test"
    },
    {
      "code": "burst-no-credential",
      "severity": "note",
      "title": "No credential is resolvable for provider \"hetzner-cloud\"",
      "detail": "The adapter is a plan, not a running integration: nothing can be provisioned, and nothing can be billed either. Mint a scoped token into the declared protected location when the owner authorises the first run.",
      "fatal": false,
      "scope": "hetzner-cloud"
    },
    {
      "code": "burst-no-credential",
      "severity": "note",
      "title": "No credential is resolvable for provider \"github\"",
      "detail": "The adapter is a plan, not a running integration: nothing can be provisioned, and nothing can be billed either. Mint a scoped token into the declared protected location when the owner authorises the first run.",
      "fatal": false,
      "scope": "github"
    },
    {
      "code": "burst-price-unverified",
      "severity": "note",
      "title": "Hetzner CPX41 burst node's price is unverified",
      "detail": "Every euro on this page is arithmetic on a number nobody has checked against an invoice. Verify before the first authorised run, not after the first bill.",
      "fatal": false,
      "scope": "hetzner-cpx41"
    },
    {
      "code": "burst-snapshot-is-recurring",
      "severity": "note",
      "title": "Hetzner CPX41 burst node: keeping a warm image costs €0.14/month",
      "detail": "12 GB at €0.01/GB/month, every month, whether or not anything boots from it. Expiry is 30 days. This is the recurring cost trade: it buys back roughly 45s of cold-boot provisioning per run.",
      "fatal": false,
      "scope": "hetzner-cpx41"
    },
    {
      "code": "burst-price-unverified",
      "severity": "note",
      "title": "Hetzner CAX31 burst node (ARM)'s price is unverified",
      "detail": "Every euro on this page is arithmetic on a number nobody has checked against an invoice. Verify before the first authorised run, not after the first bill.",
      "fatal": false,
      "scope": "hetzner-cax31"
    },
    {
      "code": "burst-snapshot-is-recurring",
      "severity": "note",
      "title": "Hetzner CAX31 burst node (ARM): keeping a warm image costs €0.12/month",
      "detail": "10 GB at €0.01/GB/month, every month, whether or not anything boots from it. Expiry is 30 days. This is the recurring cost trade: it buys back roughly 45s of cold-boot provisioning per run.",
      "fatal": false,
      "scope": "hetzner-cax31"
    },
    {
      "code": "burst-price-unverified",
      "severity": "note",
      "title": "GitHub Actions hosted runner's price is unverified",
      "detail": "Every euro on this page is arithmetic on a number nobody has checked against an invoice. Verify before the first authorised run, not after the first bill.",
      "fatal": false,
      "scope": "github-actions"
    },
    {
      "code": "burst-gap-arm-not-x86",
      "severity": "note",
      "title": "The cheapest burst target cannot build the artefacts this estate runs",
      "detail": "hetzner-cax31 is ARM. Container images built there will not run on VIN or home1 without emulation, and benchmark numbers taken there are not comparable with anything else in the estate. It is eligible only for architecture-portable work, which today is the test suite.",
      "fatal": false,
      "scope": "operator"
    }
  ],
  "source": "/app/config/burst.json"
}