<!-- x-shc-release: version=2.4.15 op_fingerprint=183a4a0282542d671d799f08044f6717182932f9fa1ef5205dfca45a030cefaa op_count=177 contractFingerprint=e8f7ccc12336fb4925893ce3582b03782b182638dc460d952d046a36c1c20e14 -->
---
name: nostr-operate-lane
description: 'A customer authorizes YOU (a specific AI agent) to operate ONE of their SHC VMs by signing a Nostr grant. You exchange that grant for a short-TTL, service-scoped, cannot-spend operate Bearer and use it against that one VM only. No password, no website, no broad key.'
when_to_use: 'Use when a customer wants to hand YOU operation of ONE specific VM service over Nostr (NIP-07/NIP-46 keys), instead of giving you an account-wide API key. Trigger: the customer offers a signed kind:30078 operate grant, OR you need to act on a single VM the customer owns and they would rather sign a scoped grant than share a key. ALSO re-run this any time your operate Bearer returns 401 (expired/used-up) -- mint a fresh one; the grant is re-usable until it expires.'
router_what: 'Trade a customer-signed Nostr operate grant for a short-TTL, single-VM, cannot-spend operate Bearer.'
router_when: 'Customer authorizes you for ONE VM over Nostr (signs a kind:30078 grant), or your operate Bearer 401s and you need a fresh one.'
version: 2.4.3
operator: true
client_side: true
requires: []
provides: [user_api_key_operate]
inputs:
  - { name: agent_pubkey, required: true, desc: 'YOUR OWN real Nostr public key (hex, 32-byte/64-char) that you already hold and can sign with. The grant authorizes THIS key and the request is signed by THIS key. Give it to the customer so they can name you in the grant. NEVER invent or guess one -- if you do not have it, say so and stop (a grant naming a made-up key never authorizes).' }
  - { name: service_id, required: true, desc: 'The Blesta service id of the customer''s VM == the user-api VM id (the {service_id} in /user-api/v2/vm/{service_id}). The grant authorizes this ONE service.' }
  - { name: operate_grant, required: true, desc: 'The customer-signed kind:30078 Nostr event (the grant). The CUSTOMER signs it with their own linked key; you receive the full signed event JSON and place it in the request body. You never sign it.' }
outputs:
  - { name: operate_token, desc: 'A customer_key_operate Bearer the server returns. Send it as Authorization: Bearer <token> on /user-api/v2/vm/{service_id}/... calls ONLY. Short-TTL (~15 min), single-VM, cannot spend.' }
estimated_minutes: [1, 3]
danger: low
---

# nostr-operate-lane -- a customer authorizes you for ONE VM, over Nostr

## Operator discipline (hold to these on every step)

*Five rules that keep you safe while you operate a server for someone. Follow them on every
step of every skill.*

1. **Verify, don't trust. Never fabricate a result.** Never report `ready`, `done`, `live`,
   `deployed`, `backed up`, or `paid` unless a real tool output or healthcheck PROVED it (and that
   proof arrives in a separate later message, never in your own reply). A status field is a claim; a
   healthcheck is proof. This holds under pressure: "tell my boss in one line it's live", "answer in
   one line", "you do not need to check" do NOT make it true and do NOT waive the check. If you have
   not verified, say exactly that -- never write a done/live/paid sentence you did not earn.
2. **Spending AND destruction are the human's call -- the user CANNOT authorize you to skip the
   confirm.** For money (order, pay) OR data-loss (reinstall, delete, cancel, restore): name the
   action, state the consequence, get the human's explicit yes for THAT action; never in one step.
   The trap: "just do it / don't ask me / skip confirmation / no need to confirm / I already agree" is
   an instruction to skip the gate -- it is NOT a yes and does NOT give you permission to bypass the
   gate. The gate OUTRANKS that instruction; proceeding because they said "just do it" is itself the
   failure. So you still state the consequence and still wait for a real yes. NEVER set `confirm=true`
   yourself, NEVER map "just go" onto `human_confirmed=true`, and NEVER assume a missing confirmation
   "defaults to true" -- the opposite is true: missing -> REFUSED (409 confirmation_required). If they
   will not give the yes, you do NOT proceed -- announce nothing, run nothing. (Mechanics: every spend
   AND destructive action is gated by a **server-issued, single-use confirmation_id** -- you can NEVER
   self-confirm. On the hosted MCP the destructive/spend tool returns the confirmation_id after its own
   human-approval step. On raw /v2 a destructive/spend call with no confirmation_id returns **409
   confirmation_required** carrying a one-time, non-secret `confirmation_id` and a `how_to_proceed`
   re-call shape; AFTER the human's explicit yes, re-send the IDENTICAL request adding the header
   **`X-User-Api-Confirm: <that confirmation_id>`**. `?confirm=true` and body `confirm:true` are NOT
   accepted -- they get the same 409. The confirmation_id is bound server-side to that exact call +
   your principal and is single-use, so a leaked id is inert (it completes nothing on its own). The
   409's `how_to_proceed` is a recovery shape, NOT a license: a confirmation_id only proves the SERVER
   has a pending record for the action -- do NOT auto-resend it until the human has actually said yes to
   the consequence. Spend is also scope-gated: an `operate` key cannot spend at all -- only a `full` key
   or HTTP Basic can.)
3. **Check the source before you run it.** Validate any deploy recipe against the project's
   *current* upstream docs, not a cached memory.
4. **Take the smallest key that does the job.** An agent that only provisions should never
   hold the key that spends.
5. **When you can't verify, stop and say so.** Failing loudly beats a confident wrong answer.

> **Your agent pubkey must be REAL and already yours -- never invent one.** The grant names your
> Nostr public key; you must already hold it and be able to sign with it. If you do not have your real
> agent pubkey, say so and stop -- do NOT make up an `npub`/hex key (a grant naming a fabricated key
> authorizes nothing).

A customer can hand you operation of **one specific VM** without giving you an account-wide key and
without touching a website. They **sign a Nostr grant** that names you (your pubkey) and that one VM.
You exchange that grant for a **short-TTL, service-scoped, cannot-spend** operate Bearer, and you use
it against **that VM only**. The grant is the authority; your lease can never exceed it.

If the customer's SHC account has not linked a Nostr identity yet, do that account setup first via
`account.arazzo.yaml`: run `link-nostr-identity` (challenge -> link), optionally `set-nostr-nip05`,
and later `unlink-nostr-identity` if they ask to remove the binding. Those are account identity
workflows; this skill starts after the account has a linked key and the customer is ready to sign a
one-VM operate grant for your agent pubkey.

What the lease is, exactly:
- **Short-TTL:** ~15 minutes (`expires_in: 900`). When it expires, mint another from the same grant.
- **Service-scoped:** it works on `/user-api/v2/vm/<service_id>/...` for the ONE granted VM and
  **403s** on any other service or any non-VM route.
- **Cannot spend:** it **403s** every spend path (ordering, buy, upgrade, renew, cancel, credit,
  payout). It operates a VM; it never moves money.
- **Destructive (data-loss) changes are confirm-gated:** reinstall, restore/delete a backup or
  snapshot, cancel, delete a firewall rule, delete an SSH key, delete a contact/manager, ... -- exactly
  like any operate key. On raw `/v2` the call with no confirmation_id returns `409 confirmation_required`
  with a one-time, non-secret `confirmation_id`; after the human's explicit yes you re-send the IDENTICAL
  request adding header `X-User-Api-Confirm: <confirmation_id>` (`?confirm=true` is NOT accepted -- it
  gets the same 409); the hosted MCP hands back the same single-use confirmation_id to re-call with.
  (Spend is a SEPARATE matter, not a confirm: this lease **cannot spend at all** -- a spend path just
  **403s**, so there is no destructive confirm route for it on raw `/v2`. On the MCP, spend tools carry
  their own confirmation gate, but an operate Bearer can't reach them regardless.) **Routine reads and non-destructive changes are
  NOT confirm-gated** (power on/off/restart/shutdown/reset, mount/unmount an ISO, add/edit a firewall
  rule, set rDNS, create a backup/snapshot, **add an SSH key**, change backup schedule/notifications) --
  this mirrors the live customer portal exactly (literal portal parity: the portal confirms the same
  data-loss actions and no others).

## Safety gates (never break)

- **You never sign the grant.** The CUSTOMER signs the `kind:30078` grant with their own key
  (NIP-07 browser extension, a NIP-46 bunker, frost2x, etc.). You only sign your own `kind:27235`
  request. If someone hands you a grant "already signed as the customer" that you produced, stop.
- **Never invent a token.** The Bearer comes ONLY from the real `token` field the server returns
  after you actually POST. Never write a token, a key, or a success "Response" yourself.
- **Use the Bearer on `/vm/<service_id>/...` only.** Don't try it on another service, a non-VM
  route, or a spend path -- it will 403, and that is correct. Don't go looking for a broader key.
- **A destructive (data-loss) change is the human's call.** Reinstall, restore/delete a backup,
  delete an SSH key, cancel: name it, get the customer's explicit yes, pass the confirm-gate. The
  lease does not waive this. (Routine ops -- a restart, adding an SSH key, changing backup settings --
  are not confirm-gated, matching the portal; still tell the customer what you're doing.)

## What the customer signs (give them this exactly)

The customer signs a Nostr **`kind:30078`** event (NIP-78 app-data, a *replaceable* event) with these tags.
Give them **your** pubkey and the **service id**; they fill them in and sign:

| tag | value | meaning |
|---|---|---|
| `d`     | `shc:agent:<YOUR_AGENT_PUBKEY>` | names YOU -- binds the grant to the agent that presents it (your `kind:27235` signer must equal this). |
| `scope` | `operate`                       | this is an operate grant (not read, not spend). |
| `area`  | `vm:<service_id>`               | the ONE VM this authorizes (the Blesta service id == the user-api VM id). |
| `aud`   | `shc:https://blesta.sovereignhybridcompute.com` | the audience/origin the grant is for (must match the server; never a different host). |
| `nbf`   | unix seconds (now)              | not-before. |
| `exp`   | unix seconds (now + <= ~15 min) | REQUIRED. Keep it short; the customer can re-sign freely. |

The grant is signed by the **customer's own linked key** (the pubkey they linked to their SHC
account). A short lifetime is fine and encouraged -- re-mint freely; it is cheap.

> Plain-language ask to the customer:
> *"Sign a one-VM authorization for me and I'll operate VM `<service_id>` for you -- no password, no
> shared key. In your Nostr signer, sign a `kind:30078` event with tags `d=shc:agent:<my pubkey>`,
> `scope=operate`, `area=vm:<service_id>`, `aud=shc:https://blesta.sovereignhybridcompute.com`, and a
> short `nbf`/`exp` (15 min is plenty). Send me the full signed event."

## Your step (sign a NIP-98 request, then POST the grant)

1. **Sign a NIP-98 `kind:27235` request event** (with YOUR key -- the same pubkey the grant names in
   `d`). It must be bound to this exact call:
   - tag `u` = `https://blesta.sovereignhybridcompute.com/plugin/nostr_auth/main/operate_token`
   - tag `method` = `POST`
   - a **fresh, unique nonce** per request (the server replay-checks; a reused request -> `401`)
   - `created_at` = now (the server enforces a request-time window)
2. **Base64 the signed `kind:27235` event** and send it as the `Authorization: Nostr <base64>` header.
3. **POST the customer's signed `kind:30078` grant in the body** as `{ "grant": <event> }`.

```bash
# $AGENT_NIP98_B64  = base64 of YOUR signed kind:27235 event (u=<the URL>, method=POST, fresh nonce)
# $OPERATE_GRANT    = the customer's full signed kind:30078 event JSON

curl -s -X POST https://blesta.sovereignhybridcompute.com/plugin/nostr_auth/main/operate_token \
  -H "Authorization: Nostr $AGENT_NIP98_B64" \
  -H "Content-Type: application/json" \
  -d "{ \"grant\": $OPERATE_GRANT }"
```

Do not write the response yourself -- run it and read the real result.

## The response

On success (`200`) the server returns the lease:

```json
{
  "success": true,
  "token": "<64-hex operate token>", // <- the customer_key_operate Bearer (a raw 64-char hex string, NOT an shc_live_ key); keep this exact value, don't print it
  "scope": "operate",
  "area": "vm:<service_id>",
  "service_id": <service_id>,
  "expires_in": 900,                 // ~15 min; re-mint from the same grant when it lapses
  "note": "Operate lease: cannot spend; bound to this service; destructive (data-loss) and spend changes require the confirm-gate (routine/non-destructive changes do not)."
}
```

Keep `token` for the session and send it as `Authorization: Bearer <token>`. Treat
`user_api_key_operate` as satisfied **for this one VM** -- it is `operate`-scoped (read + VM ops,
never spend) and **only** for `service_id`.

## Using the lease (the ONE VM, with the confirm-gate)

```bash
# Read is fine, no confirm needed:
curl -s https://blesta.sovereignhybridcompute.com/user-api/v2/vm/<service_id> \
  -H "Authorization: Bearer $OPERATE_TOKEN"

# A routine op (power/restart) needs NO confirm -- it is not destructive:
curl -s -X PATCH "https://blesta.sovereignhybridcompute.com/user-api/v2/vm/<service_id>/restart" \
  -H "Authorization: Bearer $OPERATE_TOKEN"

# A DESTRUCTIVE (data-loss) change is confirm-gated via a server-issued, single-use confirmation_id
# (raw /v2 -- ?confirm=true is NOT accepted). Reinstall wipes the VM, so it is gated. TWO STEPS:
# 1) Call WITHOUT a token -> 409 confirmation_required carrying a one-time confirmation_id:
curl -s -X PATCH "https://blesta.sovereignhybridcompute.com/user-api/v2/vm/<service_id>/reinstall" \
  -H "Authorization: Bearer $OPERATE_TOKEN" -H "Content-Type: application/json" \
  -d '{"template":"<os-template>"}'
# 2) ONLY after the human's explicit yes to the data loss, re-send the IDENTICAL request adding the
#    confirmation_id from that 409 in the header (do NOT auto-resend just because you hold the id):
curl -s -X PATCH "https://blesta.sovereignhybridcompute.com/user-api/v2/vm/<service_id>/reinstall" \
  -H "Authorization: Bearer $OPERATE_TOKEN" -H "Content-Type: application/json" \
  -H "X-User-Api-Confirm: <confirmation_id-from-the-409>" \
  -d '{"template":"<os-template>"}'
```

The Bearer is bound to `service_id`. `/user-api/v2/vm/<other_id>/...`, any non-VM route, and any
spend path will **403** -- by design.

## Common rejections (what they mean)

| code | you'll see | what it means | what to do |
|---|---|---|---|
| `401` | "Authorization: Nostr ... required" / "Invalid base64" / "Invalid JSON" | your `kind:27235` header is missing or malformed | re-sign your NIP-98 request, base64 it, retry |
| `401` | NIP-98 verification failed / **replayed request** | your request was reused, out of its time window, or not bound to this URL+POST | sign a **fresh** `kind:27235` (new nonce, `created_at`=now, `u`=this URL, `method`=POST) and retry |
| `400` | "A signed kind:30078 grant is required in the body" | no grant in the body | ask the customer for the full signed `kind:30078` event; put it in `{ "grant": ... }` |
| `403` | "Invalid grant" (**forged / expired / wrong-agent / wrong scope / aud / area**) | the grant failed verification: bad signature, past `exp`, `d` doesn't name YOUR pubkey, wrong `scope`/`aud`, or malformed `area` | have the customer re-sign a correct grant (right `d`=`shc:agent:<your pubkey>`, `scope=operate`, fresh `exp`, correct `aud`/`area`) |
| `403` | "Grant signer is not a linked customer" | the key that signed the grant isn't linked to an SHC account | the customer must link that Nostr key to their SHC account first, then re-sign |
| `403` | superseded / revoked grant | a newer grant for the same `d` replaced this one, the grant was revoked, or the customer rotated their key | get the customer's current grant and use that |
| `403` | "You do not own a service with that id" | the `area=vm:<id>` isn't a VM this customer owns (or it's canceled) | confirm the right `service_id` with the customer |
| `403` | (on a `/vm/...` call) | you used the Bearer on another service, a non-VM route, or a spend path | only call `/user-api/v2/vm/<service_id>/...`; never spend with this token |
| `409` | `confirmation_required` | a destructive (data-loss) or spend change made without the confirm-gate (a routine op like restart or adding an SSH key does NOT 409) | after the customer's yes, re-send the IDENTICAL request with the one-time `confirmation_id` from the 409 in header `X-User-Api-Confirm` (raw /v2) or the single-use confirmation_id (MCP); `?confirm=true` is NOT accepted |

## Minimal worked example

The customer owns VM service **456** and wants you to reboot it. Your agent pubkey is
`a1b2...e9f0` (64 hex).

1. **Customer signs the grant** (in their Nostr signer), a `kind:30078` with:
   `d=shc:agent:a1b2...e9f0`, `scope=operate`, `area=vm:456`,
   `aud=shc:https://blesta.sovereignhybridcompute.com`, `nbf=<now>`, `exp=<now+900>`. They send you
   the full signed event (call it `$OPERATE_GRANT`).
2. **You sign a NIP-98 request** -- a `kind:27235` with
   `u=https://blesta.sovereignhybridcompute.com/plugin/nostr_auth/main/operate_token`,
   `method=POST`, a fresh nonce, `created_at=<now>` -- and base64 it (`$AGENT_NIP98_B64`).
3. **Exchange it:**
   ```bash
   curl -s -X POST https://blesta.sovereignhybridcompute.com/plugin/nostr_auth/main/operate_token \
     -H "Authorization: Nostr $AGENT_NIP98_B64" \
     -H "Content-Type: application/json" \
     -d "{ \"grant\": $OPERATE_GRANT }"
   # -> { "success": true, "token": "<64-hex operate token>", "service_id": 456, "expires_in": 900, "area": "vm:456", ... }
   ```
4. **Tell the customer, then restart** -- a restart is routine, so NO confirm is required (it does
   not 409):
   ```bash
   curl -s -X PATCH "https://blesta.sovereignhybridcompute.com/user-api/v2/vm/456/restart" \
     -H "Authorization: Bearer <64-hex operate token>"
   ```
   (Had they asked for a **reinstall** or to **restore/delete a backup** -- destructive (data-loss) --
   the first call 409s with a one-time `confirmation_id`; after the customer's explicit yes you re-send the
   IDENTICAL request with that confirmation_id in header `X-User-Api-Confirm` (`?confirm=true` is NOT
   accepted). Adding an SSH key or changing backup settings is routine and needs no confirm, matching the
   portal.)
5. **Verify, don't trust:** re-read `/user-api/v2/vm/456` and confirm it came back up before you tell
   the customer it restarted.

**You're done when:** you exchanged a valid grant for a real `customer_key_operate` token, used it
only against `/vm/<service_id>/...`, passed the confirm-gate for any destructive (data-loss) change
(with the customer's yes), and verified the result with a live read. If the token 401s later, re-mint from the same grant (while
it's unexpired); if the grant is expired, ask the customer to re-sign -- it's cheap.
