picoclaw

picoclaw

Repo: Sipeed/picoclaw Category: Edge / Embedded Agents

picoclaw is a Go-native AI assistant from Sipeed designed to run on $10 hardware — RISC-V boards like the LicheeRV-Nano, Raspberry Pi Zero, and older Android devices. Its footprint is under 10 MB of RAM. Agents run as headless daemons, not interactive CLIs: no browser, no developer at a terminal, config baked into the device image at flash time.

Ravi fits naturally here because the edge device pattern maps directly to what Ravi solves: headless auth, provisioned email and phone for verification flows, and a secrets vault that travels with the deployment rather than requiring manual credential re-entry.

The Three Challenges picoclaw Users Face

1. No browser for ravi auth login

ravi auth login opens a device-code browser flow. A headless daemon can’t do this. The solution is RAVI_ACCESS_TOKEN — extract a token on your dev machine once, inject it as an environment variable at deploy time.

2. Config must be baked in at flash time

picoclaw agents load config from a flat JSON file when the device boots. They don’t run setup scripts interactively. Ravi supports this: write the identity config directly to .ravi/config.json before flashing, and the agent has a fully provisioned identity from first boot.

3. CLI binary availability on RISC-V / ARM

Check ravi-hq/ravi releases for the target architecture before building your device image. If the CLI binary is available for your board, subprocess calls work. If not, use the REST API directly — all Ravi operations are available over HTTPS with a Bearer token.

Setup: Provision on Your Dev Machine First

Identity provisioning requires a one-time interactive step on your dev machine. The provisioned identity and token are then baked into the device image or injected at deploy time.

# On your dev machine (has a browser for auth)
ravi auth login
ravi identity create "picoclaw-$(hostname)-01"
ravi get email --json   # → { "email": "picoclaw-...-a1b2@raviapp.com" }
ravi get phone --json   # → { "phone": "+15551234567" }

# Extract the access token for device injection
ACCESS_TOKEN=$(jq -r '.access_token' ~/.ravi/auth.json)
IDENTITY_UUID=$(ravi identity list --json | jq -r '.[0].uuid')

Option A: RAVI_ACCESS_TOKEN Injection

Pass the token as an environment variable at device boot. This works for Docker, systemd units, cloud VMs, and any system that supports env var injection.

# systemd unit (device)
[Service]
Environment=RAVI_ACCESS_TOKEN=eyJhbGci...
Environment=RAVI_CONFIG_DIR=/etc/picoclaw/ravi
ExecStart=/usr/local/bin/picoclaw-agent
# Docker
docker run -e RAVI_ACCESS_TOKEN="$ACCESS_TOKEN" sipeed/picoclaw-agent

The Ravi CLI picks up RAVI_ACCESS_TOKEN and skips file-based auth entirely. No ~/.ravi/auth.json required on the device.

Option B: Config File Baked into Device Image

Write the identity config directly into the image at build time — the agent has its identity from first boot without any network call.

# Build step (on dev machine)
mkdir -p device-image/etc/picoclaw/ravi

# Write the identity config
cat > device-image/etc/picoclaw/ravi/config.json <<EOF
{
  "identity": "picoclaw-device-01",
  "identity_uuid": "$IDENTITY_UUID"
}
EOF

# Inject the auth token via the image secrets layer
cat > device-image/etc/picoclaw/ravi/auth.json <<EOF
{
  "access_token": "$ACCESS_TOKEN",
  "token_type": "Bearer"
}
EOF

# Mount the config dir when the device boots
export RAVI_CONFIG_DIR=/etc/picoclaw/ravi

Runtime Patterns

Subprocess (CLI available)

If the Ravi CLI binary is available for your architecture:

package main

import (
    "encoding/json"
    "os/exec"
)

func raviGet(args ...string) (string, error) {
    cmd := exec.Command("ravi", args...)
    out, err := cmd.Output()
    if err != nil {
        return "", err
    }
    return string(out), nil
}

func getAgentEmail() (string, error) {
    out, err := raviGet("get", "email", "--json")
    if err != nil {
        return "", err
    }
    var result map[string]string
    json.Unmarshal([]byte(out), &result)
    return result["email"], nil
}

REST API (no CLI required)

When the CLI binary isn’t available for the target architecture, call Ravi’s REST API directly with a plain HTTP client. This works on any Go target that supports HTTPS:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

const raviBase = "https://ravi.app/api"

func raviRequest(method, path string) ([]byte, error) {
    token := os.Getenv("RAVI_ACCESS_TOKEN")
    identityUUID := os.Getenv("RAVI_IDENTITY_UUID") // set at deploy time

    req, _ := http.NewRequest(method, raviBase+path, nil)
    req.Header.Set("Authorization", "Bearer "+token)
    if identityUUID != "" {
        req.Header.Set("X-Ravi-Identity", identityUUID)
    }

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// Poll SMS inbox for an OTP code (retry loop, no bare sleep)
func pollSMSOTP(maxRetries int, intervalSec int) (string, error) {
    for i := 0; i < maxRetries; i++ {
        body, err := raviRequest("GET", "/sms/conversations/?unread=true")
        if err == nil {
            var conversations []map[string]interface{}
            if json.Unmarshal(body, &conversations) == nil && len(conversations) > 0 {
                preview := fmt.Sprintf("%v", conversations[0]["latest_preview"])
                if preview != "" {
                    return preview, nil
                }
            }
        }
        time.Sleep(time.Duration(intervalSec) * time.Second)
    }
    return "", fmt.Errorf("OTP not received after %d retries", maxRetries)
}

Token Refresh for Long-Running Daemons

picoclaw agents run continuously. Tokens expire. Add a refresh check at startup:

#!/bin/sh
# startup.sh — runs before the picoclaw agent daemon

# Refresh the token if it's within 24h of expiry
curl -s -X POST https://ravi.app/api/auth/token/refresh/ \
  -H "Authorization: Bearer $RAVI_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  | jq -r '.access_token' > /tmp/ravi_token_new

NEW_TOKEN=$(cat /tmp/ravi_token_new)
if [ -n "$NEW_TOKEN" ] && [ "$NEW_TOKEN" != "null" ]; then
    export RAVI_ACCESS_TOKEN="$NEW_TOKEN"
fi

exec /usr/local/bin/picoclaw-agent

Identity Naming Convention for Edge Fleets

For a fleet of edge devices, use deterministic naming so you can map identities back to devices:

# Per-device identity (provisioned during factory setup)
ravi identity create "picoclaw-$(DEVICE_SERIAL)-prod"

# Per-fleet pool identity (shared across replaceable devices)
ravi identity create "picoclaw-fleet-east-01"

Delete and re-provision when a device is decommissioned:

ravi identity delete "picoclaw-${OLD_SERIAL}-prod"

Architecture Summary

StepWhereHow
Provision identityDev machineravi identity create + ravi auth login
Extract tokenDev machinejq -r '.access_token' ~/.ravi/auth.json
Inject tokenBuild / deployRAVI_ACCESS_TOKEN env var or baked auth.json
Use at runtimeDeviceSubprocess (CLI) or REST API (no CLI)
Refresh tokenDevice startupPOST /api/auth/token/refresh/

Next Steps