---
name: agentvm-mpp
description: Provision a bounded temporary AgentVM through MPP, preferring a Tempo-backed session when live discovery enables it and falling back to a Stripe Link fixed-budget run, with idempotent recovery, safe receipt handling, and SSH verification.
---

# AgentVM MPP

Use this skill when a user asks an agent to create, deploy, launch, or spin up a temporary AgentVM that can be paid for through the Machine Payments Protocol.

## Source of truth

Inspect the live resources before paying:

- AgentVM package: https://www.npmjs.com/package/@wundercorp/agentvm
- MPP root: https://mpp.agentvm.sh/
- Agent instructions: https://mpp.agentvm.sh/llms.txt
- OpenAPI: https://mpp.agentvm.sh/openapi.json
- Fixed runs: https://mpp.agentvm.sh/compute/runs
- Sessions: https://mpp.agentvm.sh/compute/sessions

Do not guess current profiles, limits, payment methods, request fields, or CLI flags when discovery can answer them.

## Default safety boundary

Unless the user explicitly authorizes a different amount, use a maximum spend of `2.00 USD`. Hard-cap new VM runtime at `3540` seconds (59 minutes) unless a different product flow is explicitly requested.

Default to `hetzner-shell`. Managed desktop provisioning is not supported; use a server and the post-provision RealVNC guidance when a GUI is needed. Do not switch to MCP Sandbox unless the user requests it or the live run catalog makes the shell profile unavailable.

A paid request must have exactly one persisted `requestId` and one persisted `idempotencyKey`. Reuse both for every retry or recovery attempt. Never create new identifiers merely because the result of a paid request is uncertain.

Never expose private SSH keys, bearer tokens, MPP Session tokens, wallet credentials, payment credentials, or other secret capabilities in logs or the final response.

## Install

AgentVM:

```bash
npm install -g @wundercorp/agentvm
```

Tempo CLI:

```bash
curl -fsSL https://tempo.xyz/install | bash
```

If required by the current Tempo CLI:

```bash
tempo add request
tempo wallet login
```

For the fixed-budget fallback, AgentVM installs Stripe Link CLI support. If `link-cli` is still unavailable, use the current official `@stripe/link-cli` package.

## Prepare SSH and request state

Create protected local state before any payment:

```bash
mkdir -p .agentvm-mpp
chmod 700 .agentvm-mpp
```

If `.agentvm-mpp/request.env` already exists for this attempt, load it and reuse its IDs and SSH key path. Otherwise create one pair of request identifiers and a dedicated per-attempt SSH key path:

```bash
REQUEST_ID="run-$(date +%s)-$(openssl rand -hex 4)"
IDEMPOTENCY_KEY="$(openssl rand -hex 24)"
SSH_KEY_PATH="$PWD/.agentvm-mpp/keys/$REQUEST_ID"
MAX_SPEND="2.00"
MAX_RUNTIME_SECONDS="3540"
printf 'REQUEST_ID=%q\nIDEMPOTENCY_KEY=%q\nSSH_KEY_PATH=%q\nMAX_SPEND=%q\nMAX_RUNTIME_SECONDS=%q\n' "$REQUEST_ID" "$IDEMPOTENCY_KEY" "$SSH_KEY_PATH" "$MAX_SPEND" "$MAX_RUNTIME_SECONDS" > .agentvm-mpp/request.env
chmod 600 .agentvm-mpp/request.env
```

On a retry, load the same state. For an older state file without `SSH_KEY_PATH`, do not create a new identity if a response already exists or payment status is ambiguous. Only before any paid request, persist the dedicated path:

```bash
source .agentvm-mpp/request.env
if test -z "${MAX_RUNTIME_SECONDS:-}"; then
  if test -s .agentvm-mpp/provision.json || test -s .agentvm-mpp/session-recovery.json; then
    echo 'Existing paid/recovery state predates the 59-minute runtime ceiling; stop rather than changing an authorized request.' >&2
    exit 1
  fi
  MAX_RUNTIME_SECONDS="3540"
  printf 'MAX_RUNTIME_SECONDS=%q\n' "$MAX_RUNTIME_SECONDS" >> .agentvm-mpp/request.env
  chmod 600 .agentvm-mpp/request.env
fi
if test -z "${SSH_KEY_PATH:-}"; then
  if test -s .agentvm-mpp/provision.json; then
    echo 'Existing AgentVM response predates persisted SSH key state; do not create a replacement key or repurchase.' >&2
    exit 1
  fi
  SSH_KEY_PATH="$PWD/.agentvm-mpp/keys/$REQUEST_ID"
  printf 'SSH_KEY_PATH=%q\n' "$SSH_KEY_PATH" >> .agentvm-mpp/request.env
  chmod 600 .agentvm-mpp/request.env
fi
```

Do not use `~/.ssh/id_ed25519`, `~/.ssh/id_rsa`, or another personal default identity for MPP automation. A personal key may require a passphrase or depend on an interactive SSH agent, which can leave a successfully purchased VM inaccessible to a non-interactive harness.

Create the per-attempt key directory and generate the dedicated key if it is missing:

```bash
mkdir -p "$(dirname "$SSH_KEY_PATH")"
chmod 700 "$(dirname "$SSH_KEY_PATH")"
test -f "$SSH_KEY_PATH" || ssh-keygen -q -t ed25519 -N '' -f "$SSH_KEY_PATH" -C "agentvm-mpp-$REQUEST_ID"
test -f "$SSH_KEY_PATH.pub" || { echo 'AgentVM automation SSH public key is missing'; exit 1; }
chmod 600 "$SSH_KEY_PATH"
chmod 644 "$SSH_KEY_PATH.pub"
PUBLIC_KEY="$(cat "$SSH_KEY_PATH.pub")"
```

The key path is unique to the persisted `REQUEST_ID`, and the harness creates the key itself with an empty passphrase. If key creation fails, stop before payment. Only transmit `PUBLIC_KEY` to AgentVM. When inside a git repository, add `.agentvm-mpp/` to `.gitignore`.

For an older `.agentvm-mpp/request.env` that has no `SSH_KEY_PATH`, create and persist the per-attempt path only when no paid request has been sent. If a paid response already exists, do not generate a replacement identity or repurchase: recover with the original private key that matches the public key already installed on the VM.

## Choose the live payment route

Fetch `https://mpp.agentvm.sh/compute/sessions` and `https://mpp.agentvm.sh/compute/runs`.

Prefer a Tempo-backed MPP Session only when session discovery says sessions are enabled and publishes Tempo as an active settlement method. Otherwise use the fixed-budget Stripe Link run.

If a wallet or payment client requires interactive authentication that the agent cannot complete, stop before the paid request and give the user the exact login command or action.

## Tempo-backed MPP Session

Only use this route when live discovery enables it.

Build the JSON request with `jq` using the exact fields published by discovery. Include the persisted request ID, idempotency key, SSH public key, and caller-selected hard ceilings. Keep `maximumSpend` at or below the user's authorized amount and keep runtime within the published profile bounds. Persist that ceiling as `MAX_SPEND`. If the user explicitly raises it after a clearly pre-payment refusal reporting zero paid, update `MAX_SPEND` while reusing the same request identifiers. Never change the ceiling or retry when payment status is ambiguous.

Use the current Tempo CLI syntax. The expected shape is:

```bash
source .agentvm-mpp/request.env
MAX_SPEND="${MAX_SPEND:-2.00}"
MAX_RUNTIME_SECONDS="${MAX_RUNTIME_SECONDS:-3540}"
test "$MAX_RUNTIME_SECONDS" = "3540" || { echo 'This bounded flow requires a 59-minute maximum runtime.' >&2; exit 1; }
PUBLIC_KEY="$(cat "$SSH_KEY_PATH.pub")"
SESSION_REQUEST_JSON="$(jq -c -n \
  --arg requestId "$REQUEST_ID" \
  --arg idempotencyKey "$IDEMPOTENCY_KEY" \
  --arg publicKey "$PUBLIC_KEY" \
  --arg maximumSpend "$MAX_SPEND" \
  --argjson maximumRuntimeSeconds "$MAX_RUNTIME_SECONDS" \
  '{requestId:$requestId,idempotencyKey:$idempotencyKey,publicKey:$publicKey,maximumSpend:$maximumSpend,maximumRuntimeSeconds:$maximumRuntimeSeconds}')"
REQUEST_PAYLOAD_B64="$(printf '%s' "$SESSION_REQUEST_JSON" | base64 | tr -d '\r\n')"
umask 077
tempo request \
  --max-spend "$MAX_SPEND" \
  -X POST \
  -H "x-agentvm-request-payload: $REQUEST_PAYLOAD_B64" \
  --json "$SESSION_REQUEST_JSON" \
  -o .agentvm-mpp/provision.json \
  'https://mpp.agentvm.sh/compute/sessions/hetzner-shell'
chmod 600 .agentvm-mpp/provision.json
```

If the session response is pending or ambiguous, do not repurchase. If AgentVM returns `mpp_session_provisioning_failed` with `details.paid=true` and `details.recoverable=true`, the session payment has been persisted. AgentVM automatically tries alternate eligible Hetzner placement combinations for explicit capacity failures; if those are exhausted, retry the same session POST directly with the persisted request ID, idempotency key, maximum spend, and SSH public key. Do not invoke Tempo again for that recovery. An existing paid session should recover without another payment challenge. If a direct recovery request returns HTTP 402, stop rather than authorizing another payment.

For an explicit paid-session recovery, reconstruct the same JSON and use `curl` directly so no payment client can create another authorization:

```bash
source .agentvm-mpp/request.env
MAX_SPEND="${MAX_SPEND:-2.00}"
MAX_RUNTIME_SECONDS="${MAX_RUNTIME_SECONDS:-3540}"
test "$MAX_RUNTIME_SECONDS" = "3540" || { echo 'This bounded flow requires a 59-minute maximum runtime.' >&2; exit 1; }
PUBLIC_KEY="$(cat "$SSH_KEY_PATH.pub")"
SESSION_REQUEST_JSON="$(jq -c -n \
  --arg requestId "$REQUEST_ID" \
  --arg idempotencyKey "$IDEMPOTENCY_KEY" \
  --arg publicKey "$PUBLIC_KEY" \
  --arg maximumSpend "$MAX_SPEND" \
  --argjson maximumRuntimeSeconds "$MAX_RUNTIME_SECONDS" \
  '{requestId:$requestId,idempotencyKey:$idempotencyKey,publicKey:$publicKey,maximumSpend:$maximumSpend,maximumRuntimeSeconds:$maximumRuntimeSeconds}')"
REQUEST_PAYLOAD_B64="$(printf '%s' "$SESSION_REQUEST_JSON" | base64 | tr -d '\r\n')"
RECOVERY_PATH=".agentvm-mpp/session-recovery.json"
RECOVERY_HTTP_STATUS="$(curl -sS -o "$RECOVERY_PATH" -w '%{http_code}' \
  -X POST \
  -H 'content-type: application/json' \
  -H "x-agentvm-request-payload: $REQUEST_PAYLOAD_B64" \
  --data "$SESSION_REQUEST_JSON" \
  'https://mpp.agentvm.sh/compute/sessions/hetzner-shell')"
chmod 600 "$RECOVERY_PATH"
case "$RECOVERY_HTTP_STATUS" in
  200|201) mv "$RECOVERY_PATH" .agentvm-mpp/provision.json ;;
  202) jq '{ok,type,paid,sessionId,requestId,message}' "$RECOVERY_PATH" ;;
  402) echo 'Unexpected payment challenge during paid-session recovery; do not authorize it.' >&2; exit 1 ;;
  *) jq '{ok,code,message,details:{paid:.details.paid,recoverable:.details.recoverable,provider:.details.provider,attemptedPlacements:.details.attemptedPlacements}}' "$RECOVERY_PATH"; exit 1 ;;
esac
```

## Stripe Link fixed-budget fallback

Use this route when sessions are disabled or Tempo settlement is unavailable.

Authenticate only if required by the current CLI:

```bash
link-cli auth status
link-cli auth login
```

Build the request body with `jq` and keep the budget within the authorized maximum:

```bash
source .agentvm-mpp/request.env
MAX_RUNTIME_SECONDS="${MAX_RUNTIME_SECONDS:-3540}"
test "$MAX_RUNTIME_SECONDS" = "3540" || { echo 'This bounded flow requires a 59-minute maximum runtime.' >&2; exit 1; }
test -n "${SSH_KEY_PATH:-}" || { echo 'Missing persisted AgentVM SSH key path'; exit 1; }
PUBLIC_KEY="$(cat "$SSH_KEY_PATH.pub")"

umask 077
LINK_RESULT_PATH=".agentvm-mpp/link-result.json"
link-cli mpp pay -X POST 'https://mpp.agentvm.sh/compute/runs/hetzner-shell' \
  -H 'content-type: application/json' \
  -d "$(jq -n \
    --arg requestId "$REQUEST_ID" \
    --arg idempotencyKey "$IDEMPOTENCY_KEY" \
    --arg budget '2.00' \
    --arg publicKey "$PUBLIC_KEY" \
    --argjson maximumRuntimeSeconds "$MAX_RUNTIME_SECONDS" \
    '{requestId:$requestId,idempotencyKey:$idempotencyKey,budget:$budget,publicKey:$publicKey,maximumRuntimeSeconds:$maximumRuntimeSeconds}')" \
  --context 'Purchase one predefined AgentVM shell box for development/testing with a hard 59-minute runtime ceiling. Maximum authorized spend is $2.00. The VM is temporary and automatically expires.' \
  --format json > "$LINK_RESULT_PATH"
chmod 600 "$LINK_RESULT_PATH"
jq -e -r '.body' "$LINK_RESULT_PATH" > .agentvm-mpp/provision.json
chmod 600 .agentvm-mpp/provision.json
jq -e . .agentvm-mpp/provision.json >/dev/null
rm -f "$LINK_RESULT_PATH"
```

Treat the paid MPP result as secret-bearing. Current Link CLI supports structured `--format json` output and returns the merchant response in its `body` field, so keep stdout redirected to the protected intermediate file and extract the body without printing it. Never `cat`, `tee`, or run `jq .` to stdout on either file. If the installed CLI schema differs, inspect `link-cli mpp pay --schema` or `--help` before the paid request and use the equivalent structured capture path; do not make a paid request with raw terminal output just to discover the shape.

## Safe receipt

Once the complete response exists on disk:

```bash
chmod 600 .agentvm-mpp/provision.json
agentvm mpp receipt .agentvm-mpp/provision.json
```

Use the safe receipt output to identify operational details without dumping sensitive tokens or dashboard capability fragments.

## Verify over SSH

Parse the SSH host, user, and port from the response and connect with the exact persisted key. Use `BatchMode=yes` and `IdentitiesOnly=yes` so SSH cannot silently fall back to a passphrase-protected personal identity:

```bash
SSH_HOST="$(jq -r '.vm.ssh.host // .vm.ipv4 // .vm.ipv6 // empty' .agentvm-mpp/provision.json)"
SSH_USER="$(jq -r '.vm.ssh.user // "user"' .agentvm-mpp/provision.json)"
SSH_PORT="$(jq -r '.vm.ssh.port // 22' .agentvm-mpp/provision.json)"
test -n "$SSH_HOST" || { echo 'AgentVM SSH host is missing'; exit 1; }
ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$SSH_KEY_PATH" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST"
```

Run:

```bash
set -e
hostname
whoami
uname -a
cat /etc/os-release
df -h /
free -h || true
if command -v cloud-init >/dev/null 2>&1; then sudo cloud-init status --wait || true; fi
for attempt in $(seq 1 90); do
  if sudo test -f /var/lib/agentvm-wundercorp-tools-ready; then break; fi
  if sudo test -f /var/lib/agentvm-wundercorp-tools-failed; then sudo systemctl restart agentvm-wundercorp-tools.service || true; fi
  sleep 5
done
agentvm-tools-status
for command_name in node npm python3 argent agentvm om doku codex claude bs; do command -v "$command_name" >/dev/null; done
echo AGENTVM_OK
```

The task is not complete until `AGENTVM_OK` succeeds after the preinstalled runtimes and agent tools verify, or the agent can clearly explain why initialization or SSH verification could not be completed.

## Recovery rule

If provisioning is pending, interrupted, fails with `mpp_session_provisioning_failed` or `mpp_run_provisioning_failed`, times out after payment, or returns an ambiguous result, never solve uncertainty by creating fresh purchase identifiers. AgentVM automatically cycles through several eligible Hetzner placement combinations for explicit provider capacity failures within the same paid request. For a paid Tempo session with `details.paid=true` and `details.recoverable=true`, retry the exact same session POST directly with the persisted `REQUEST_ID`, `IDEMPOTENCY_KEY`, maximum spend, and SSH public key without invoking Tempo again. For a paid fixed-budget run, retry the same run POST directly with the exact persisted request identity without invoking Link again. Existing paid requests recover without a new charge; if a direct recovery request returns HTTP 402, stop rather than approving another payment.

## Final response

Return only safe operational fields:

- provisioning status
- payment mode used
- profile, provider, region, and selected capacity when returned
- amount paid or authorized and spend ceiling
- creation time, expiration time, and remaining runtime
- SSH host, user, and port
- HTTPS endpoint when returned
- SSH smoke-test result
- local receipt path

Do not return private keys, bearer tokens, session tokens, wallet credentials, payment credentials, or secret dashboard capabilities.

For a user-facing guide and copy/paste harness prompt, open https://agentvm.sh/docs or https://agentvm.sh/DEPLOY.md.
