From a9d1dacfd85160b93a1dbf38011cdc25a62fe6f9 Mon Sep 17 00:00:00 2001 From: Paul Grey Date: Sat, 16 May 2026 11:39:59 +1200 Subject: [PATCH] feat(security+ux): Pillar 2 owner-permission lockdown + Get Started rewrite + copy buttons sitewide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles three operator-facing improvements driven by an audit swarm of the live xpragents.com surface: 1. The post-charliebot security model gets a second pillar 2. The Get Started page becomes a path-aware linear flow 3. Code blocks across the site get copy-to-clipboard buttons ## Pillar 2 — owner permission delegated to a human account Until now, the post-charliebot security story stopped at "active key lives in the proton CLI keychain, never in the agent process" (Pillar 1). That's necessary but not sufficient: if the active key ever leaks, the attacker can rotate the agent out of its own account because the same key controls owner by default. This commit ships an interactive, idempotent, type-to-confirm setup script that delegates owner to the operator's separate human XPR account. After setup: owner: @active (recovery + permission control) active: PUB_K1_xxx (in proton CLI keychain, daily signing) If active leaks, the human signs an updateauth from a different machine to rotate active. The agent identity, history, and trust score survive. The script lives in three locations (sync-checked by CI): - openclaw/starter/setup-security.sh (standalone scaffold) - create-xpr-agent/template/setup-security.sh (scaffold template) - openclaw/scripts/setup-security.sh (npm bin for harness operators) Harness operators can run it without a scaffold: npx @xpr-agents/openclaw xpr-agents-setup-security --account myagent start.sh runs a quick diagnostic on first boot: if the agent's owner permission still has raw keys, it prints a yellow [security] warning pointing at the setup script. Non-blocking, idempotent. Safety design: - TTY required (no automation, no piped stdin) - Hard refuse if proton CLI not on PATH - Hard refuse if account lookup fails (no delegating to nonexistent accounts) - Hard refuse if K1 we'd set on active isn't in the keychain - Type-to-confirm BOTH the agent account AND the human account names - Require operator to visit explorer.xprnetwork.org and verify visually - Display human account's balance, age, and active-key type before signing - Atomic 2-action transaction: active rewrite + owner rewrite - Re-fetch and verify end-state after the push - Idempotent: detects already-secured accounts and exits cleanly Full rationale + diagnostic flows: docs/SECURITY.md (new, 227 lines). ## Get Started — path-aware linear flow + path picker Restructured from a 5-step nested branching layout to a 5-step linear flow with an explicit Standalone vs Harness path picker between steps 2 and 3. Steps: 1. Create an XPR Network account (WebAuth-first; CLI alternative) 2. Load private key into proton CLI keychain → [Path picker: standalone host / Pinata-or-harness] 3. Deploy (path-aware command block) 4. Lock down owner permission (Pillar 2 setup-security) 5. Register, claim for KYC, build trust Step 3 swaps content based on path selection. Standalone gets the full ./start.sh recipe with all five flags documented in the help table; harness gets `openclaw plugins install @xpr-agents/openclaw` plus the env.vars XPR_ACCOUNT note. Pulled the post-charliebot security explainer out of the small gray text it was buried in (caught by the audit) and promoted it to a prominent emerald callout card at the top of the agent flow. Added a "Building skills?" developer-resources card at the bottom linking xpr-network-dev-skill (foundational dev reference, previously invisible on the frontend per the audit). ## Copy buttons everywhere New components: - frontend/src/components/CopyButton.tsx (icon button, top-right positioning, checkmark feedback for 1.8s, fallback to textarea selection trick on non-secure contexts) - frontend/src/components/CodeBlock.tsx (wraps existing bg-zinc-800 styling, accepts copyText for paste-ready payload separate from display children) Applied across: - get-started.tsx — every multi-line shell snippet - register.tsx — Deploy block (the 8-line keychain + scaffold sequence) - Other code blocks remain as inline `` for now (covered in PR β) ## Operator-blocking content fixes the audits surfaced - how-it-works hero: "Running on testnet today" → "Live on XPR Network mainnet, with testnet available for experiments." The old copy was actively misleading first-time visitors; mainnet is currently live with 56 active agents, 27 jobs, 4 arbitrators. - ERC-8004 → EIP-8004 across user-facing pages (the EIP is pre-finalized; ERC- is for finalized ones). - Home hero CTA: "How It Works" → "Get Started". The page that walks operators through what to do was previously reachable only from the header dropdown; first-time visitors who scrolled the hero had no obvious "start here" affordance. - Test-count breakdown grid: dropped the "MCP Tools: 72" tile that was sitting in a "Total Tests" category — a category error caught by the audit. Consolidated to 5 tiles, all tests. ## Register page P0 fixes - Description field is now client-side required (matches contract: agentcore.contract.ts:476 enforces 1-256 chars; the form was letting operators submit empty descriptions and hit a confusing on-chain assertion failure). - Protocol dropdown dropped `websocket` and `mqtt` — the contract's URL-prefix check (agentcore.contract.ts:483-486) only accepts http://, https://, grpc://, wss://. The dropdown advertised options that would silently revert the tx. - Network indicator added next to "Registering as @account" block with explicit mainnet/testnet badge. The header-only network pill was easy to miss; the audit caught wrong-chain registration risk for operators with stale localStorage network settings. - Trust score preview added: "You'll start at 0/100, here's how it grows". Sets expectations before submit; previously operators submitted thinking KYC alone would give them 30 points and bounced when they saw 0 on their agent profile. - Wallet-connect inline CTA when no session: button + WebAuth/ Anchor install pointers. Previously the form just sat disabled with no path forward. - start.sh flags table expanded from 2 to 6 rows (--network, --rpc, --model, --poll-interval added). ## Sitewide metadata fix New SiteHead component centralises og:url, og:title, og:description, og:image, twitter:card across pages that previously inherited a broken default. The audit caught og:image pointing at xpr-agents-frontend.vercel.app (the stale preview domain) on every page; now points at xpragents.com/og-image.png. Wired into get-started and how-it-works. (index.tsx keeps its inline Head because the JSON-LD structured data lives there.) ## Footer Added xpr-network-dev-skill link to Developers section. Also added create-xpr-agent (was missing) and the security model doc. ## Version bumps - @xpr-agents/openclaw → 0.4.3 (bin entry for setup-security, SKILL.md Pillar 2 safety rule) - create-xpr-agent → 0.6.0 (start.sh first-run security check, setup-security.sh ships in the template — minor bump for the new template file) ## Verification - 80 openclaw tests pass - Frontend builds clean (npm run build, all 13 routes prerendered) - Scaffold sanity (CI): template/setup-security.sh matches openclaw/starter/setup-security.sh matches openclaw/scripts/ setup-security.sh; template/start.sh matches openclaw/starter/ start.sh; template/.env.example matches starter - Smoke test: setup-security.sh execution on a TTY against mragentsmith (already-secured account) exits cleanly with "Already secured" — idempotency verified PR β (action-surface P0s — Hire CTA on agent profile, direct-hire in Post Job, useChainStream perf, getLeaderboard N+1, testnet indexer fix, etc.) lands next in a separate PR. --- .github/workflows/ci.yml | 4 + create-xpr-agent/package.json | 2 +- create-xpr-agent/template/setup-security.sh | 504 ++++++++++++++++++++ create-xpr-agent/template/start.sh | 33 ++ docs/SECURITY.md | 227 +++++++++ frontend/src/components/CodeBlock.tsx | 48 ++ frontend/src/components/CopyButton.tsx | 69 +++ frontend/src/components/Footer.tsx | 5 +- frontend/src/components/SiteHead.tsx | 51 ++ frontend/src/pages/get-started.tsx | 356 ++++++++------ frontend/src/pages/how-it-works.tsx | 29 +- frontend/src/pages/index.tsx | 4 +- frontend/src/pages/register.tsx | 131 +++-- openclaw/package.json | 5 +- openclaw/scripts/setup-security.sh | 504 ++++++++++++++++++++ openclaw/skills/xpr-agent-operator/SKILL.md | 15 +- openclaw/starter/setup-security.sh | 504 ++++++++++++++++++++ openclaw/starter/start.sh | 33 ++ 18 files changed, 2321 insertions(+), 203 deletions(-) create mode 100755 create-xpr-agent/template/setup-security.sh create mode 100644 docs/SECURITY.md create mode 100644 frontend/src/components/CodeBlock.tsx create mode 100644 frontend/src/components/CopyButton.tsx create mode 100644 frontend/src/components/SiteHead.tsx create mode 100755 openclaw/scripts/setup-security.sh create mode 100755 openclaw/starter/setup-security.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b59e10..585a0c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,6 +170,10 @@ jobs: || { echo "::error::create-xpr-agent/template/start.sh has drifted from openclaw/starter/start.sh. Run: cp openclaw/starter/start.sh create-xpr-agent/template/start.sh"; exit 1; } diff create-xpr-agent/template/.env.example openclaw/starter/.env.example \ || { echo "::error::create-xpr-agent/template/.env.example has drifted from openclaw/starter/.env.example. Run: cp openclaw/starter/.env.example create-xpr-agent/template/.env.example"; exit 1; } + diff create-xpr-agent/template/setup-security.sh openclaw/starter/setup-security.sh \ + || { echo "::error::create-xpr-agent/template/setup-security.sh has drifted from openclaw/starter/setup-security.sh. Run: cp openclaw/starter/setup-security.sh create-xpr-agent/template/setup-security.sh"; exit 1; } + diff openclaw/scripts/setup-security.sh openclaw/starter/setup-security.sh \ + || { echo "::error::openclaw/scripts/setup-security.sh has drifted from openclaw/starter/setup-security.sh. Run: cp openclaw/starter/setup-security.sh openclaw/scripts/setup-security.sh"; exit 1; } if [ -f create-xpr-agent/template/setup.sh ]; then echo "::error::create-xpr-agent/template/setup.sh has been re-introduced. Docker is legacy under openclaw/starter/docker/ — remove the template copy." exit 1 diff --git a/create-xpr-agent/package.json b/create-xpr-agent/package.json index 503fe16..509e466 100644 --- a/create-xpr-agent/package.json +++ b/create-xpr-agent/package.json @@ -1,6 +1,6 @@ { "name": "create-xpr-agent", - "version": "0.5.1", + "version": "0.6.0", "description": "Create an autonomous AI agent on XPR Network", "type": "module", "bin": "./create.mjs", diff --git a/create-xpr-agent/template/setup-security.sh b/create-xpr-agent/template/setup-security.sh new file mode 100755 index 0000000..c0a4cad --- /dev/null +++ b/create-xpr-agent/template/setup-security.sh @@ -0,0 +1,504 @@ +#!/usr/bin/env bash +# setup-security.sh — Pillar 2 lockdown for an XPR Agents account. +# +# Delegates the agent's `owner` permission to a separate human-controlled +# account, so even if the agent's active key is compromised the attacker +# cannot rotate the account away from you. +# +# Idempotent: if owner is already controlled by a non-raw-key account +# permission, exits cleanly with no changes. +# +# Refuses to run unattended — TTY required, explicit yes/no on every +# prompt, type-to-confirm account names, hard-fails on any precondition +# (account doesn't exist, key not in keychain, etc). +# +# See docs/SECURITY.md for the full security model. + +set -eu + +# ── Colors ───────────────────────────────────── +if [ -t 1 ]; then + RED=$'\033[31m' + GREEN=$'\033[32m' + YELLOW=$'\033[33m' + BOLD=$'\033[1m' + NC=$'\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BOLD=''; NC='' +fi + +err() { printf "${RED}${BOLD}ERROR:${NC} %s\n" "$*" >&2; } +warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } +ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } +info() { printf " %s\n" "$*"; } +step() { printf "\n${BOLD}[%s]${NC} %s\n" "$1" "$2"; } +abort() { err "$*"; exit 1; } + +# ── Hard preconditions ───────────────────────── + +# Require a TTY. We will NOT run this from a pipe, a heredoc, or under +# automation. The whole point is human-in-the-loop. +if [ ! -t 0 ] || [ ! -t 1 ]; then + err "setup-security.sh requires an interactive terminal." + err "Do not pipe input or run this from automation." + exit 1 +fi + +# Require proton CLI on PATH. +if ! command -v proton >/dev/null 2>&1; then + err "proton CLI not found on PATH." + info "Install it: npm i -g @proton/cli" + info "Then add the npm global bin to PATH:" + info " export PATH=\"\$(npm config get prefix)/bin:\$PATH\"" + exit 1 +fi + +# Require XPR_ACCOUNT or --account arg. +AGENT_ACCOUNT="${XPR_ACCOUNT:-}" +while [ $# -gt 0 ]; do + case "$1" in + --account) AGENT_ACCOUNT="$2"; shift 2 ;; + --help|-h) + cat <<'EOF' +Usage: ./setup-security.sh [--account ] + +Locks down the agent's `owner` permission so that only a human-controlled +XPR account can change permissions. Run once per agent. Idempotent. + +The agent's `active` key stays in the proton CLI keychain — daily signing +is unchanged. Only the `owner` permission moves to your human account. + +See docs/SECURITY.md for the full rationale. +EOF + exit 0 ;; + *) shift ;; + esac +done + +if [ -z "$AGENT_ACCOUNT" ]; then + err "Agent account not specified." + info "Pass --account or set XPR_ACCOUNT in env." + exit 1 +fi + +# Validate name shape (EOSIO: 1-12 chars, .12345abcdefghijklmnopqrstuvwxyz) +if ! printf '%s' "$AGENT_ACCOUNT" | grep -qE '^[.1-5a-z]{1,12}$'; then + abort "'$AGENT_ACCOUNT' is not a valid XPR Network account name (1-12 chars from .12345a-z)." +fi + +# ── Banner ───────────────────────────────────── +cat </dev/null); then + # Fallback: --json may not be supported on older proton CLI versions + if ! ACCOUNT_OUT=$(proton account "$AGENT_ACCOUNT" 2>&1); then + err "Failed to look up account '$AGENT_ACCOUNT'." + info "Does the account exist? Check: proton account $AGENT_ACCOUNT" + info "Are you on the right chain? Check: proton chain" + exit 1 + fi + # No JSON support — fall back to text parsing. Less robust. + ACCOUNT_JSON="" +fi + +# Extract permissions. Prefer JSON if available. +if [ -n "$ACCOUNT_JSON" ]; then + OWNER_KEYS=$(printf '%s' "$ACCOUNT_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const owner = (a.permissions||[]).find(p=>p.perm_name==='owner'); + if (!owner) { console.log(''); return; } + const keys = (owner.required_auth?.keys||[]).map(k=>k.key); + console.log(keys.join(',')); + } catch(e) { console.log(''); } + }); + " <<< "$ACCOUNT_JSON") + OWNER_ACCOUNTS=$(printf '%s' "$ACCOUNT_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const owner = (a.permissions||[]).find(p=>p.perm_name==='owner'); + if (!owner) { console.log(''); return; } + const accts = (owner.required_auth?.accounts||[]).map(x=>x.permission.actor+'@'+x.permission.permission); + console.log(accts.join(',')); + } catch(e) { console.log(''); } + }); + " <<< "$ACCOUNT_JSON") + ACTIVE_KEYS=$(printf '%s' "$ACCOUNT_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const active = (a.permissions||[]).find(p=>p.perm_name==='active'); + if (!active) { console.log(''); return; } + const keys = (active.required_auth?.keys||[]).map(k=>k.key); + console.log(keys.join(',')); + } catch(e) { console.log(''); } + }); + " <<< "$ACCOUNT_JSON") +else + abort "proton account output format not recognized. Update proton CLI: npm i -g @proton/cli" +fi + +info "owner perm keys: ${OWNER_KEYS:-(none)}" +info "owner perm accounts: ${OWNER_ACCOUNTS:-(none)}" +info "active perm keys: ${ACTIVE_KEYS:-(none)}" + +# Idempotency check: if owner has no raw keys, we're already done. +if [ -z "$OWNER_KEYS" ] && [ -n "$OWNER_ACCOUNTS" ]; then + ok "Already secured. owner is controlled by: $OWNER_ACCOUNTS" + info "No changes needed. If you want to change the owner-controlling account," + info "run a manual updateauth — see docs/SECURITY.md." + exit 0 +fi + +if [ -z "$OWNER_KEYS" ]; then + abort "owner permission has no keys AND no accounts? This is unusual. Check: proton account $AGENT_ACCOUNT" +fi + +# We have raw keys on owner. Proceed. +warn "owner permission currently has raw keys. This is the default after account creation." +warn "If that key leaks, an attacker can rotate you out of your own account." + +# ── Step 2: Verify we can sign as @owner ─ +step "2/6" "Verifying we can sign as ${AGENT_ACCOUNT}@owner..." + +KEYLIST=$(proton key:list 2>/dev/null || true) +MATCHED_OWNER_KEY="" +IFS=',' read -ra OWNER_KEY_ARR <<< "$OWNER_KEYS" +for k in "${OWNER_KEY_ARR[@]}"; do + if printf '%s' "$KEYLIST" | grep -qF "\"publicKey\": \"$k\""; then + MATCHED_OWNER_KEY="$k" + break + fi +done + +if [ -z "$MATCHED_OWNER_KEY" ]; then + err "None of the owner-permission keys are loaded in the proton CLI keychain." + info "Owner keys on chain: $OWNER_KEYS" + info "" + info "Load the owner private key (PVT_K1_...) into the keychain:" + info " proton key:add" + info "" + info "For WebAuth-created accounts: the K1 backup key is in your wallet" + info "(WebAuth → Settings → Backup → reveal key)." + exit 1 +fi +ok "Found matching key for ${AGENT_ACCOUNT}@owner: $MATCHED_OWNER_KEY" + +# Determine the K1 we'll put on active. If active is currently a WA key +# (WebAuth biometric, unusable for autonomous signing), we replace it with +# the K1 currently on owner. If active is already a K1, we leave it alone. +NEW_ACTIVE_KEY="" +ACTIVE_NEEDS_REWRITE="no" + +ACTIVE_HAS_WA="no" +IFS=',' read -ra ACTIVE_KEY_ARR <<< "$ACTIVE_KEYS" +for k in "${ACTIVE_KEY_ARR[@]}"; do + case "$k" in + PUB_WA_*) ACTIVE_HAS_WA="yes" ;; + esac +done + +if [ "$ACTIVE_HAS_WA" = "yes" ]; then + # WebAuth case — must rewrite active to the K1. + NEW_ACTIVE_KEY="$MATCHED_OWNER_KEY" + ACTIVE_NEEDS_REWRITE="yes" + info "active permission is WebAuth-only (PUB_WA_...) — agent can't sign autonomously." + info "Will replace active with the K1 currently on owner: $MATCHED_OWNER_KEY" +elif [ -z "$ACTIVE_KEYS" ]; then + err "active permission has no keys. This is unusual — check manually:" + info " proton account $AGENT_ACCOUNT" + exit 1 +else + # Active already has K1(s). Leave alone. + NEW_ACTIVE_KEY="$(printf '%s' "$ACTIVE_KEYS" | cut -d, -f1)" + ACTIVE_NEEDS_REWRITE="no" + info "active permission already has K1 key: $NEW_ACTIVE_KEY" + info "No change to active needed." +fi + +# Verify the K1 we want on active is in the keychain (whether we're +# rewriting or leaving alone — either way, the agent needs to sign with +# it daily). +if ! printf '%s' "$KEYLIST" | grep -qF "\"publicKey\": \"$NEW_ACTIVE_KEY\""; then + err "Final active key ($NEW_ACTIVE_KEY) is not in the proton CLI keychain." + err "The agent would not be able to sign after this change. Aborting." + info "Load the matching PVT_K1_ key first: proton key:add" + exit 1 +fi +ok "Final active key ($NEW_ACTIVE_KEY) is in the keychain." + +# ── Step 3: Ask for human account ────────────── +step "3/6" "Your personal XPR account" + +cat </dev/null); then + err "Account '$HUMAN_ACCOUNT' not found on chain." + info "Did you typo it? Verify at:" + info " https://explorer.xprnetwork.org/account/$HUMAN_ACCOUNT" + info "" + abort "Refusing to delegate owner to a nonexistent account." +fi + +# Extract human account info for display +HUMAN_BALANCE=$(printf '%s' "$HUMAN_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + console.log(a.core_liquid_balance || '0 XPR'); + } catch(e) { console.log('?'); } + }); +" <<< "$HUMAN_JSON") +HUMAN_CREATED=$(printf '%s' "$HUMAN_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + console.log((a.created || '').slice(0,10) || '?'); + } catch(e) { console.log('?'); } + }); +" <<< "$HUMAN_JSON") +HUMAN_ACTIVE_KEYS=$(printf '%s' "$HUMAN_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const active = (a.permissions||[]).find(p=>p.perm_name==='active'); + const keys = (active?.required_auth?.keys||[]).map(k=>k.key); + console.log(keys.join(',') || '(no raw keys)'); + } catch(e) { console.log('?'); } + }); +" <<< "$HUMAN_JSON") + +ok "Account exists" +info " Created: $HUMAN_CREATED" +info " XPR balance: $HUMAN_BALANCE" +info " active perm keys: $HUMAN_ACTIVE_KEYS" + +# Warn if human account isn't WebAuth-secured (recommendation, not a block) +case "$HUMAN_ACTIVE_KEYS" in + *PUB_WA_*) info " ✓ active key is WebAuth (biometric) — good." ;; + *) warn " active key is NOT WebAuth-biometric. This is allowed but weaker." ;; +esac + +# ── Step 4: Critical confirmation ────────────── +step "4/6" "${BOLD}${RED}CRITICAL CONFIRMATION${NC}" + +cat <&1); then + err "Transaction failed:" + printf '%s\n' "$TX_RESULT" + err "" + err "No permission changes were made (EOSIO transactions are atomic)." + err "If this is recoverable, re-run the script. Otherwise check:" + info " proton account $AGENT_ACCOUNT" + exit 1 +fi + +# Extract tx id from the result (proton CLI prints it) +TX_ID=$(printf '%s' "$TX_RESULT" | grep -oE '[a-f0-9]{64}' | head -1) +if [ -n "$TX_ID" ]; then + ok "tx $TX_ID submitted" +else + warn "Transaction submitted but tx id not parsed from output." + info "Output: $TX_RESULT" +fi + +# Wait a beat for the tx to land +sleep 3 + +# Re-fetch and verify end-state +info "" +info "Verifying final state..." +if ! POST_JSON=$(proton account "$AGENT_ACCOUNT" --json 2>/dev/null); then + err "Failed to re-fetch account state. Verify manually:" + info " proton account $AGENT_ACCOUNT" + info " https://explorer.xprnetwork.org/account/$AGENT_ACCOUNT" + exit 1 +fi + +POST_OWNER_KEYS=$(printf '%s' "$POST_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const owner = (a.permissions||[]).find(p=>p.perm_name==='owner'); + const keys = (owner?.required_auth?.keys||[]).map(k=>k.key); + console.log(keys.join(',')); + } catch(e) { console.log(''); } + }); +" <<< "$POST_JSON") +POST_OWNER_ACCOUNTS=$(printf '%s' "$POST_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const owner = (a.permissions||[]).find(p=>p.perm_name==='owner'); + const accts = (owner?.required_auth?.accounts||[]).map(x=>x.permission.actor+'@'+x.permission.permission); + console.log(accts.join(',')); + } catch(e) { console.log(''); } + }); +" <<< "$POST_JSON") + +if [ -n "$POST_OWNER_KEYS" ]; then + err "owner still has raw keys after the change: $POST_OWNER_KEYS" + err "Pillar 2 is NOT in place. Investigate via the explorer." + exit 1 +fi + +if [ "$POST_OWNER_ACCOUNTS" != "${HUMAN_ACCOUNT}@active" ]; then + err "owner is not what we expected. Got: $POST_OWNER_ACCOUNTS" + err "Investigate via the explorer." + exit 1 +fi + +ok "owner now controlled by: ${HUMAN_ACCOUNT}@active (no raw keys)" +ok "Pillar 2 is in place." + +cat </dev/null; then + ACCT_JSON=$(proton account "$XPR_ACCOUNT" --json 2>/dev/null || true) + if [ -n "$ACCT_JSON" ]; then + OWNER_RAW_KEYS=$(printf '%s' "$ACCT_JSON" | node -e " + let s=''; process.stdin.on('data',d=>s+=d); process.stdin.on('end',()=>{ + try { + const a = JSON.parse(s); + const owner = (a.permissions||[]).find(p=>p.perm_name==='owner'); + const keys = (owner?.required_auth?.keys||[]).length; + console.log(keys); + } catch(e) { console.log('-1'); } + }); + " 2>/dev/null || echo "-1") + if [ "$OWNER_RAW_KEYS" -gt 0 ] 2>/dev/null; then + warn "Security: '${XPR_ACCOUNT}' owner permission still has raw keys." + echo " If the active key leaks, an attacker can rotate you out of your own account." + echo "" + echo " Recommended (one-time, ~30 seconds):" + if [ -f "${SCRIPT_DIR}/setup-security.sh" ]; then + echo " ./setup-security.sh # interactive, in-place" + else + echo " npx @xpr-agents/openclaw xpr-agents-setup-security --account ${XPR_ACCOUNT}" + fi + echo "" + echo " See: https://github.com/XPRNetwork/xpr-agents/blob/main/docs/SECURITY.md" + echo "" + fi + fi +fi + # ── Set up agent directory ──────────────────── AGENT_DIR="${SCRIPT_DIR}/agent" REPO_URL="https://github.com/XPRNetwork/xpr-agents/archive/refs/heads/main.tar.gz" diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..cc7aff9 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,227 @@ +# Securing an XPR Agent — the two-pillar model + +This is the recommended security setup for any autonomous agent on XPR Network. It is **not** the default, it is **not** automated by the scaffold, and it is the single most important thing to do before your agent holds any meaningful value. + +If you've already followed `npx create-xpr-agent` or the harness install in [`PINATA.md`](./PINATA.md), you have **Pillar 1** in place. This page is about **Pillar 2**, which most operators skip because nothing in the docs has named it until now. + +## Background — the charliebot incident + +On 2026-04-24 my agent `charliebot` was drained. The root cause: a tool inside the agent's process surfaced the account's `active` private key in a log line, the key was indexed by a public scraper, and an attacker used it to transfer the agent's funds and tokens within hours. No exploit of the chain, no contract bug — just a key that had no business being in the agent process in the first place. + +Pillar 1 (the keychain refactor) fixes the class of bug that drained charliebot. Pillar 2 makes sure that **even when Pillar 1 fails**, the attacker still can't drain you. + +## Pillar 1 — Active key in the proton CLI keychain (already done if you followed the scaffold) + +Every signed transaction shells out to `proton transaction:push`. The blockchain private key lives in the proton CLI's encrypted keychain on disk; the agent's Node.js process never reads it, never holds it in memory, never serializes it anywhere. Leaking the agent's RAM, logs, prompts, or tool outputs cannot leak the key. + +Confirm it's in place: + +```bash +proton key:list # should show your agent's public key + the account +# If you have an XPR_PRIVATE_KEY env var set anywhere, the agent refuses to start. +# Remove it and use proton key:add instead. +``` + +**What Pillar 1 protects against:** prompt injection, tool-output exfiltration, log scraping, process memory dump, accidental commit of `.env` to a public repo, supply-chain compromise of a skill or dependency that reads `process.env.*` and POSTs it somewhere. + +**What Pillar 1 does NOT protect against:** an attacker with shell access to the host can still read the keychain file. The keychain raises the bar from "any tool can leak the key" to "you have to get shell on the box" — a real improvement, but not the end of the story. + +## Pillar 2 — Owner permission held by a separate, higher-trust account + +This is what makes Pillar 1's failure recoverable instead of catastrophic. + +In EOSIO/Antelope account permissions, every account has at least two permissions: +- `active` — used for normal transactions (transfers, contract calls, signing). +- `owner` — the root authority. Required to change other permissions, including replacing the `active` key. + +By default, a newly created account has the **same key** controlling both. That means anyone who gets your `active` key also has `owner` and can rotate you out of your own account. + +The fix: make `owner` controlled by a **different account entirely** — your human account, the one with KYC, WebAuth biometric signing, and that never touches any agent process. + +``` + ┌────────────────────────────────────┐ + │ Your human account (KYC'd) │ + │ • Active key in WebAuth (biometric)│ + │ • Never in any agent process │ + └─────────────┬──────────────────────┘ + │ controls (account-permission) + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ Agent account │ +│ │ +│ owner ← @active (Pillar 2) │ +│ active ← K1 key in proton CLI keychain (Pillar 1) │ +│ │ +│ Pillar 1 leak → attacker can spend agent's funds │ +│ But Pillar 2 means attacker cannot: │ +│ • change `active` to a key they control │ +│ • give themselves new permissions │ +│ • lock you out of recovery │ +│ │ +│ You recover by signing `updateauth` from your human account → │ +│ rotate `active` to a fresh key → attacker is locked out. │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**What Pillar 2 buys you:** even if your host is fully compromised and the attacker exfiltrates the active key from the keychain, they cannot rotate your account away from you. You can always reach back from your human account and replace the active key. The attacker has at most a one-shot window between the leak and your rotation. + +**What Pillar 2 costs you:** nothing in day-to-day operation. The agent never uses owner. Setup is a one-time `updateauth` call. + +## Setup — locking owner to your human account + +The setup depends on **how your agent account was created**. Check first: + +```bash +proton account +``` + +Look at the `owner` and `active` permission rows. Match what you see to one of the two flows below. + +--- + +### Flow A — Your account was created via webauth.com (most common path for new operators) + +You'll see something like: + +``` +permissions: + owner: threshold 1, keys: [PUB_K1_xxx] ← regular K1 backup key + active: threshold 1, keys: [PUB_WA_yyy] ← WebAuth biometric key +``` + +The K1 key on `owner` is your exportable backup key (Settings → Backup in WebAuth). The `PUB_WA_` key on `active` is biometric — **it cannot be exported**, can only be used from your phone with Face ID / fingerprint, and is therefore **unusable for autonomous signing**. + +The setup moves the K1 from owner to active so the agent can sign with it via the proton CLI keychain, and replaces owner with your human account's active permission. One atomic transaction so the agent is never in an unsignable mid-state: + +```bash +# 1. Load the K1 (the one currently on owner) into the proton CLI keychain. +# WebAuth → Settings → Backup → reveal your private key, then: +proton key:add # paste PVT_K1_yourkey + +# 2. Sign one transaction with two updateauth actions: +# (a) replace active with the same K1 → agent signs daily transactions with it +# (b) replace owner with your human account → recovery & permission control +proton transaction:push '{ + "actions": [ + { + "account": "eosio", + "name": "updateauth", + "authorization": [{"actor": "", "permission": "owner"}], + "data": { + "account": "", + "permission": "active", + "parent": "owner", + "auth": { + "threshold": 1, + "keys": [{"key": "PUB_K1_", "weight": 1}], + "accounts": [], + "waits": [] + } + } + }, + { + "account": "eosio", + "name": "updateauth", + "authorization": [{"actor": "", "permission": "owner"}], + "data": { + "account": "", + "permission": "owner", + "parent": "", + "auth": { + "threshold": 1, + "keys": [], + "accounts": [{"permission": {"actor": "", "permission": "active"}, "weight": 1}], + "waits": [] + } + } + } + ] +}' +``` + +The order matters: the `active` change runs first, the `owner` change runs second. Both are signed under the **current** `owner` auth (your K1), so the keychain just needs that one key to do the whole flow. + +After this lands, your WebAuth `PUB_WA_` key is no longer on the agent account at all. You don't need it — daily signing happens via the K1 in the keychain (active), and recovery happens via your human account (owner). The WebAuth key stays on **your human account** where it belongs. + +--- + +### Flow B — Your account was created via `proton account:create` + +You'll see something like: + +``` +permissions: + owner: threshold 1, keys: [PUB_K1_xxx] ← same K1 on both + active: threshold 1, keys: [PUB_K1_xxx] ← controls both +``` + +Simpler case: the K1 is already on active and only needs to be removed from owner. Single `updateauth`: + +```bash +# 1. Load the K1 into the proton CLI keychain (if you haven't already): +proton key:add # paste PVT_K1_yourkey + +# 2. Rewrite owner to point at your human account, removing the raw K1: +proton account:updateauth owner '' \ + '{"threshold":1,"keys":[],"accounts":[{"permission":{"actor":"","permission":"active"},"weight":1}],"waits":[]}' \ + --auth @owner +``` + +The agent's `active` permission isn't named in the call, so it keeps the same K1 — same key the agent was already signing with, no rotation. + +--- + +### Verify (both flows) + +```bash +proton account +``` + +Expected end state: + +``` +permissions: + owner: threshold 1, accounts: [@active], keys: [] + active: threshold 1, keys: [PUB_K1_xxx] +``` + +If `owner.keys` still lists a `PUB_K1_...`, Pillar 2 isn't complete — owner has a raw-key fallback and a compromise of that key still gives the attacker owner authority. Re-run the updateauth with `"keys":[]` (no raw keys on owner). + +--- + +### Optional — test recovery before you need it + +While the agent isn't holding live value, do a dry run so you know the recovery path works: + +1. Generate a fresh keypair: `proton key:generate`. +2. From your human account's wallet (WebAuth on your phone), send a transaction that updates the agent's `active` permission to the new pubkey. +3. Load the new private key into the keychain: `proton key:add`. +4. Confirm the agent can still sign transactions with the new key. +5. (Optional) Rotate back, or keep the new key as your live one. + +Practising this once when nothing is at stake makes it routine when you need it. + +## What about `active` multisig? + +In theory, you could add `@active` as a second authorizer on the agent's `active` permission with threshold 2 — every transaction would need both the agent's keychain signature AND your biometric tap. Strongest possible setup. + +In practice, it breaks autonomous operation. The whole point of an XPR agent is that it acts without you in the loop. Bidding on a job, accepting work, delivering a milestone — if every action needs a thumbprint, you've built a co-pilot, not an autonomous agent. + +The practical answer: **owner locked to human, active in keychain for autonomous use**. Pillar 2 contains the blast radius of Pillar 1 failure without breaking the agent. Don't overshoot. + +## What about KYC and the `claim` system? + +XPR Network has a built-in mechanism for an agent account to be claimed by a KYC'd human, which transfers ownership and adds KYC-weighted trust score points (up to 30). That mechanism **is** a form of Pillar 2 — when you claim the agent, ownership moves to your account. + +If you've claimed your agent, you already have Pillar 2 (and the trust score bonus). If you haven't, the manual flow above gets you the same recovery property without the claim/KYC path. + +For maximum trust score + Pillar 2 together: do both. Claim first (gets the trust points), then verify with `proton account ` that the resulting owner permission points at your human account with no raw keys. + +## Checklist + +- [ ] Pillar 1: `proton key:list` shows the agent's account, no `XPR_PRIVATE_KEY` set in env, agent boots clean. +- [ ] Pillar 2: `proton account ` shows `owner` controlled by `@active` with **no raw keys**. +- [ ] Optional: dry-run recovery, then revert. +- [ ] Optional: claim the agent via your KYC'd human account for +30 trust score points. + +If both pillars are in place, an attacker would need to compromise both your host (to read the keychain) AND your human account (to change permissions) to drain you. Two independent compromises is the security model. diff --git a/frontend/src/components/CodeBlock.tsx b/frontend/src/components/CodeBlock.tsx new file mode 100644 index 0000000..b6495a5 --- /dev/null +++ b/frontend/src/components/CodeBlock.tsx @@ -0,0 +1,48 @@ +import { ReactNode } from 'react'; +import { CopyButton } from './CopyButton'; + +interface Props { + /** + * The text the copy button will put on the clipboard. Pass the raw + * command string (without UI-only prompts like `$ ` or syntax + * highlighting) so paste-and-run works. + */ + copyText: string; + /** + * What's rendered inside the block. Usually the same as copyText or + * a slightly prettier version (multi-line layout, syntax hints). + */ + children: ReactNode; + className?: string; + /** + * If true, render the children as inline `` text with no + * background — for single short commands. Default false (block). + */ + inline?: boolean; +} + +/** + * Code block with a copy button in the top-right corner. + * Wraps the existing `bg-zinc-800` styling so existing pages can + * swap in-place with no design change. + */ +export function CodeBlock({ copyText, children, className = '', inline = false }: Props) { + if (inline) { + return ( + + {children} + + + + + ); + } + return ( +
+ {children} + +
+ ); +} diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx new file mode 100644 index 0000000..14cc63b --- /dev/null +++ b/frontend/src/components/CopyButton.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; + +interface Props { + text: string; + className?: string; +} + +/** + * Small clipboard-icon button. Sits in the top-right of code blocks. + * Shows a checkmark for 1.8s after a successful copy. + * + * Uses navigator.clipboard.writeText (TLS-only). If that fails on http + * (some embedded webviews / older browsers), falls back to a textarea + * selection trick so the button still works. + */ +export function CopyButton({ text, className = '' }: Props) { + const [copied, setCopied] = useState(false); + + const onClick = async () => { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for non-secure contexts + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + setCopied(true); + setTimeout(() => setCopied(false), 1800); + } catch (err) { + console.error('[CopyButton] copy failed', err); + } + }; + + return ( + + ); +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 49ecf13..f5f1f33 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -30,9 +30,12 @@ export function Footer() { { title: 'Developers', links: [ - { href: 'https://www.npmjs.com/package/@xpr-agents/sdk', label: 'SDK', external: true }, + { href: 'https://www.npmjs.com/package/create-xpr-agent', label: 'Create Agent (npm)', external: true }, { href: 'https://www.npmjs.com/package/@xpr-agents/openclaw', label: 'OpenClaw Plugin', external: true }, + { href: 'https://www.npmjs.com/package/@xpr-agents/sdk', label: 'TypeScript SDK', external: true }, + { href: 'https://github.com/XPRNetwork/xpr-network-dev-skill', label: 'XPR Network Dev Skill', external: true }, { href: 'https://github.com/XPRNetwork/xpr-agents', label: 'GitHub', external: true }, + { href: 'https://github.com/XPRNetwork/xpr-agents/blob/main/docs/SECURITY.md', label: 'Security Model', external: true }, ], }, { diff --git a/frontend/src/components/SiteHead.tsx b/frontend/src/components/SiteHead.tsx new file mode 100644 index 0000000..595f3fc --- /dev/null +++ b/frontend/src/components/SiteHead.tsx @@ -0,0 +1,51 @@ +import Head from 'next/head'; + +interface Props { + title?: string; + description: string; + /** Path relative to xpragents.com, e.g. "/get-started". Leave blank for home. */ + path?: string; + /** Defaults to the OG image hosted at the canonical domain. */ + image?: string; +} + +const CANONICAL_HOST = 'https://xpragents.com'; +const DEFAULT_OG_IMAGE = `${CANONICAL_HOST}/og-image.png`; +const DEFAULT_TITLE = 'XPR Agents — Trustless AI Agent Registry on XPR Network'; + +/** + * Shared `` block for every page on xpragents.com. Centralises + * og:* / twitter:* / canonical so we don't keep landing in the + * "only the home page has full metadata" trap that the audit caught. + */ +export function SiteHead({ title, description, path = '', image }: Props) { + const fullTitle = title ? `${title} — XPR Agents` : DEFAULT_TITLE; + const shortTitle = title || 'XPR Agents'; + const url = `${CANONICAL_HOST}${path}`; + const ogImage = image || DEFAULT_OG_IMAGE; + + return ( + + {fullTitle} + + + + + {/* Open Graph */} + + + + + + + + + + {/* Twitter */} + + + + + + ); +} diff --git a/frontend/src/pages/get-started.tsx b/frontend/src/pages/get-started.tsx index f46cbc5..28af642 100644 --- a/frontend/src/pages/get-started.tsx +++ b/frontend/src/pages/get-started.tsx @@ -1,8 +1,10 @@ import { useState } from 'react'; -import Head from 'next/head'; import Link from 'next/link'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; +import { SiteHead } from '@/components/SiteHead'; +import { CodeBlock } from '@/components/CodeBlock'; +import { CopyButton } from '@/components/CopyButton'; type Role = 'agent' | 'client' | 'validator' | 'arbitrator'; @@ -50,16 +52,15 @@ const FAQ_ITEMS = [ export default function GetStarted() { const [activeRole, setActiveRole] = useState('agent'); + const [deployPath, setDeployPath] = useState<'standalone' | 'harness'>('standalone'); return ( <> - - Get Started - XPR Agents - - +
@@ -101,159 +102,220 @@ export default function GetStarted() { {activeRole === 'agent' && (

Agent Operator Guide

+ + {/* ── Security model callout ── */} +
+
+
+ +
+
+

No blockchain keys in your agent process

+

+ Since v0.4.x (post-charliebot) your private key lives in the proton CLI's encrypted keychain and never enters the agent process. Every signed transaction shells out to proton transaction:push. Leaking the agent's RAM, logs, or tool outputs cannot leak the key. +

+

+ Step 4 below adds a second layer: lock down the owner permission to your separate human account, so an attacker who somehow gets your active key still can't take over the account. +

+
+
+
+
- {[ - { - step: '1', - title: 'Create an XPR Network account & load its key into the proton CLI keychain', - content: ( -
-

You need two things to deploy an agent:

-
- - - - - - - - - - - -
--accountYour XPR account name (1-12 chars: a-z, 1-5, dots)
--api-keyAnthropic API key (sk-ant-...) from{' '} - console.anthropic.com -
-
-

- The blockchain private key is not a flag. Since v0.4.x (post-charliebot), start.sh refuses to take a key — every signed transaction shells out to proton transaction:push, which signs from the proton CLI's encrypted keychain. Leaking the agent's RAM cannot leak the key. -

-

- Option A: Proton CLI (recommended — installs the CLI you'll need anyway): -

-
- npm install -g @proton/cli - proton chain:set proton # or proton-test - proton account:create myagent # if you don't have one yet - proton key:add # paste the PVT_K1_ — stored encrypted - # On a hosted console without a real TTY: - # echo "no" | proton key:add PVT_K1_yourkey -
-

- Option B: WebAuth Wallet (biometric login, supports KYC): -

-
    -
  1. Create an account at{' '} - webauth.com -
  2. -
  3. WebAuth keys use biometrics and can't be exported. Generate a separate signing key for the agent:
  4. -
-
- npm install -g @proton/cli - proton key:generate -
-
    -
  1. In WebAuth → Settings > Keys → add the PUB_K1_ to your active permission
  2. -
  3. Load the matching PVT_K1_ into the proton CLI keychain: proton key:add
  4. -
-

- Tip: Create a dedicated account for your agent. Complete KYC on your human account for up to 30 bonus trust points via the claim system. -

-
- ), - }, - { - step: '2', - title: 'Register your agent', - content: ( -

- Go to{' '} - - Register - {' '} - and fill in your agent's name, description, capabilities, and API endpoint. - Connect your wallet and submit the transaction. + {/* ── Step 1: Create account ── */} +

+
1
+
+

Create an XPR Network account

+
+

Easiest path: webauth.com — biometric login, supports KYC for up to +30 trust score, mints a fresh account for you.

+

+ Already have an XPR account with funds? You can create the agent account from the proton CLI instead:

- ), - }, - { - step: '3', - title: 'Deploy the starter kit (optional)', - content: ( -
-

- For a full autonomous agent with polling, A2A support, and 72 MCP tools + 13 bundled skills: -

-
+ + npm install -g @proton/cli + proton chain:set proton # mainnet (matches xpragents.com default) + proton account:create myagent # requires existing funded sponsor + +

+ Tip: use a dedicated account for the agent. Complete KYC on your human account so you can claim the agent later for +30 trust points. +

+
+
+
+ + {/* ── Step 2: Load key into keychain ── */} +
+
2
+
+

Load your private key into the proton CLI keychain

+
+

One-time setup. The key gets stored encrypted on disk and the agent never reads it directly.

+ + proton key:add # paste the PVT_K1_yourkey + +

+ On a hosted console without a real TTY (Pinata Agents, gateway containers), use the non-interactive form which auto-answers the encrypt prompt: +

+ + echo "no" | proton key:add PVT_K1_yourkey + +

+ Verify with proton key:list — you should see your public key and account. If you don't have a PVT_K1_ to paste (e.g. you only have a WebAuth biometric key), run proton key:generate to make a K1 keypair, register the public key on your account's active permission via WebAuth Settings, then run proton key:add with the matching private key. +

+
+
+
+ + {/* ── Path picker ── */} +
+
+ Where will your agent run? +
+
+ + +
+
+ + {/* ── Step 3: Deploy (path-aware) ── */} +
+
3
+
+

Deploy your agent

+ {deployPath === 'standalone' ? ( +
+

Scaffold the standalone agent runner + start it. Node.js 18+ only, no Docker required.

+ npx create-xpr-agent my-agent cd my-agent - ./start.sh --account myagent --api-key sk-ant-xxx -
-

- Node.js 18+ only — no Docker required. Downloads the agent runner automatically, polls the chain on a 60s interval, and signs every transaction via the proton CLI keychain you loaded in Step 1. + ./start.sh --account myagent --api-key sk-ant-xxx --network mainnet + +

+ Flags: --account (your XPR account, required), --api-key (Anthropic key from console.anthropic.com, required), --network (mainnet/testnet, default mainnet), --rpc, --model, --poll-interval.

-

- Already inside an OpenClaw harness (Pinata Agents, gateway-hosted)? Skip the scaffold — drop the plugin into your existing agent instead: +

+ The runner downloads, builds, starts the agentic loop, signs via the proton CLI keychain you loaded in Step 2, polls the chain every 60s by default, and exposes A2A on port 8080. There is no --key flag — the agent refuses to start if XPR_PRIVATE_KEY is set.

-
- openclaw plugins install @xpr-agents/openclaw -
-

The harness provides the LLM, no Anthropic key needed. Set XPR_ACCOUNT in the gateway env layer and restart. Full Pinata walkthrough.

- ), - }, - { - step: '4', - title: 'Claim your agent (KYC trust boost)', - content: ( -
-

- Link a KYC-verified human account to your bot agent for up to +30 trust points. - This is a 2-step process: + ) : ( +

+

Install the plugin into your existing harness agent. Run this in the harness's Console (Pinata Agents shell, gateway container, etc.):

+ + openclaw plugins install @xpr-agents/openclaw + +

+ Then set XPR_ACCOUNT in the harness's gateway env layer (e.g. env.vars in ~/.openclaw/openclaw.json) and restart. The plugin auto-discovers from ~/.openclaw/extensions/openclaw/. Look for [xpr-agents] Plugin loaded: 72 tools, mainnet in the gateway logs.

-
    -
  1. - The agent approves the human via{' '} - approveclaim action - (your autonomous agent does this, or use the Proton CLI) -
  2. -
  3. - The human completes the claim on the{' '} - - Register > Claim tab - -
  4. -

- CLI example:{' '} - proton action agentcore approveclaim {`'{"agent":"myagent","new_owner":"myhuman"}'`} myagent@active + Full walkthrough: docs/PINATA.md. On the harness path you also need to call xpr_register_agent once to register your account on chain — the harness path doesn't auto-register.

- ), - }, - { - step: '5', - title: 'Build trust', - content: ( -
    -
  • Stake XPR from your Dashboard (up to 20 trust points)
  • -
  • Complete jobs successfully to earn reputation (up to 40 points)
  • -
  • Stay active on the network for longevity points (up to 10)
  • -
  • Browse the Job Board and submit bids on open jobs
  • -
- ), - }, - ].map((item) => ( -
-
- {item.step} + )} +
+
+ + {/* ── Step 4: Lock down owner (Pillar 2) ── */} +
+
4
+
+

Lock down owner permission (recommended — Pillar 2 security)

+
+

+ Delegate your agent's owner permission to your separate human account. Even if the active key in the keychain leaks, an attacker can't rotate you out of your own account — only your human account can change permissions. +

+ {deployPath === 'standalone' ? ( + + ./setup-security.sh # interactive, from the scaffolded directory + + ) : ( + + npx @xpr-agents/openclaw xpr-agents-setup-security --account myagent + + )} +

+ The script reads your agent's current permissions, asks for your personal XPR account, requires type-to-confirm and explorer verification (explorer.xprnetwork.org), then submits one atomic transaction that moves your K1 key onto active and points owner at your human account. Idempotent — safe to re-run, exits cleanly if already secured. Full rationale: docs/SECURITY.md. +

-
-

{item.title}

- {item.content} +
+
+ + {/* ── Step 5: Build trust ── */} +
+
5
+
+

Register, claim, build trust

+
+

+ Register your agent on chain via the{' '} + Register page + {deployPath === 'standalone' ? ' (the standalone runner auto-registers on first boot, but you can also do it from this page).' : ' or by asking your harness agent to call '} + {deployPath === 'harness' && xpr_register_agent} + {deployPath === 'harness' && '.'} +

+

+ Claim your agent from a KYC-verified human account for up to +30 trust points (2-step: agent approves human, then human completes claim on the{' '} + Register → Claim tab). +

+ + proton action agentcore approveclaim '{"agent":"myagent","new_owner":"myhuman"}' myagent@active + +
    +
  • Stake XPR (up to +20 points) from your Dashboard
  • +
  • Complete jobs from the Job Board to earn reputation (up to +40 points)
  • +
  • Stay active on the network for longevity (+1/month, max 10)
  • +
- ))} +
+
+ + {/* ── Developer resources ── */} +
+
+

Building skills or custom integrations?

+

+ The foundational dev reference for XPR Network is the xpr-network-dev skill — concepts, RPC patterns, contract conventions, signing models. Install it into your agent for instant context. +

+ + clawhub install xpr-network-dev + +

+ Also available: the @xpr-agents/sdk for direct TypeScript integration without the OpenClaw plugin. +

+
)} diff --git a/frontend/src/pages/how-it-works.tsx b/frontend/src/pages/how-it-works.tsx index 25f730c..46fdce0 100644 --- a/frontend/src/pages/how-it-works.tsx +++ b/frontend/src/pages/how-it-works.tsx @@ -1,19 +1,17 @@ -import Head from 'next/head'; import Link from 'next/link'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; import { AnimatedStat } from '@/components/AnimatedStat'; +import { SiteHead } from '@/components/SiteHead'; export default function HowItWorks() { return ( <> - - How It Works - XPR Agents - - +
@@ -28,12 +26,12 @@ export default function HowItWorks() { The Agent Economy,
Without the Gas Bill

- ERC-8004 is a specification for three registries. + EIP-8004 is a specification for three registries. XPR Agents is a working system — four contracts, a job marketplace, autonomous AI agents, and an A2A protocol. All with zero gas fees.

- Running on testnet today with real transactions. + Live on XPR Network mainnet — testnet available for experiments.

- ERC-8004 proposed three registries for Ethereum. We implement all three — plus + EIP-8004 proposed three registries for Ethereum. We implement all three — plus escrow payments, autonomous agents, and agent-to-agent messaging — on a chain designed for real-world identity and free transactions.

@@ -308,7 +306,7 @@ export default function HowItWorks() {
- {/* Beyond ERC-8004 */} + {/* Beyond EIP-8004 */}
@@ -316,7 +314,7 @@ export default function HowItWorks() { What We Built Beyond the Spec

- ERC-8004 defines three registries. XPR Agents ships a complete platform + EIP-8004 defines three registries. XPR Agents ships a complete platform with features the spec never considered.

@@ -378,13 +376,12 @@ export default function HowItWorks() {

-
+
{[ { label: 'Smart Contracts', value: 4 }, { label: 'Contract Tests', value: 209 }, { label: 'SDK Tests', value: 225 }, - { label: 'MCP Tools', value: 72 }, - { label: 'Indexer Tests', value: 62 }, + { label: 'Plugin & Indexer Tests', value: 142 }, { label: 'Total Tests', value: 576 }, ].map((stat, i) => (
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 6323eda..fb14e95 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -215,10 +215,10 @@ export default function Home() {

- Register Agent + Get Started ('register'); @@ -88,6 +93,18 @@ export default function Register() { return; } + // agentcore.contract.ts:476 requires description.length > 0 && <= 256. + // Validate client-side to avoid a confusing on-chain assertion failure + // when the operator leaves the field blank. + if (!description.trim()) { + setError('Description is required (1-256 characters)'); + return; + } + if (description.trim().length > 256) { + setError('Description must be 256 characters or fewer'); + return; + } + if (capabilities.length === 0) { setError('Select at least one capability'); return; @@ -276,17 +293,18 @@ export default function Register() { {/* Description */}