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
| Step | Where | How |
|---|---|---|
| Provision identity | Dev machine | ravi identity create + ravi auth login |
| Extract token | Dev machine | jq -r '.access_token' ~/.ravi/auth.json |
| Inject token | Build / deploy | RAVI_ACCESS_TOKEN env var or baked auth.json |
| Use at runtime | Device | Subprocess (CLI) or REST API (no CLI) |
| Refresh token | Device startup | POST /api/auth/token/refresh/ |
Next Steps
- Authentication → CI & Headless Setup — full headless auth reference
- Production Patterns — token injection, ephemeral lifecycle, error handling
- CLI Reference → Environment Variables —
RAVI_ACCESS_TOKENandRAVI_CONFIG_DIR - API Endpoints — full REST API reference for no-CLI deployments