Skill: jim-counter/auto-drive-jim (version 1.0.0)

--- name: auto-drive description: Upload and download files to Autonomys Network permanent decentralized storage via Auto-Drive. Save memories as a linked-list chain for resurrection — rebuild full agent context from a single CID. metadata: openclaw: emoji: "🧬" primaryEnv: AUTO_DRIVE_API_KEY requires: bins: ["curl", "jq", "file"] env: ["AUTO_DRIVE_API_KEY"] install: - id: jq-brew kind: brew formula: jq bins: ["jq"] label: "Install jq (brew)" --- # Auto-Drive Skill Permanent decentralized storage on the Autonomys Network with linked-list memory chains for agent resurrection. ## What This Skill Does 1. **Upload files** to Auto-Drive and get back a CID (Content Identifier) — a permanent, immutable address on the Autonomys distributed storage network. 2. **Download files** from Auto-Drive using a CID — uses the authenticated API if a key is set, otherwise falls back to the public gateway. 3. **Save memories as a chain** — each memory entry is a JSON experience with a `header.previousCid` pointer, forming a linked list stored permanently on-chain. 4. **Resurrect from a chain** — given the latest CID, walk the chain backwards to reconstruct full agent history. ## When To Use This Skill - User says "save this to Auto-Drive" or "upload to Autonomys" or "store permanently" - User says "download from Auto-Drive" or provides a CID to retrieve - User says "save memory", "remember this permanently", or "checkpoint" - User says "resurrect", "recall chain", "rebuild memory", or "load history" - Any time the user wants data stored permanently and immutably on a decentralized network ## Configuration ### API Key Requires an `AUTO_DRIVE_API_KEY`. Run the guided setup script for the easiest path: ```bash scripts/setup-auto-drive.sh ``` This opens [explorer.ai3.storage](https://explorer.ai3.storage/mainnet/drive/developers) in your browser, prompts for the key, saves it to `~/.openclaw/.env`, and verifies the connection. **Manual setup:** 1. Navigate to **https://explorer.ai3.storage** (not `ai3.storage` — that's just the landing page) 2. Check authentication: if the sidebar shows a "Log In" button or lock icons on My Files / Profile / Developers, you are not signed in 3. **AI agent note:** You cannot complete OAuth yourself. Open the browser for the user and ask them to sign in via Google, GitHub, or Discord. Once the user confirms they're signed in, take over — or simply ask the user to paste the API key directly 4. Once authenticated, click **Developers** in the left sidebar 5. Click **Create API Key** and copy the generated key Then set the key via: - **Environment:** `export AUTO_DRIVE_API_KEY=your_key_here` - **OpenClaw config:** `skills.entries.auto-drive.apiKey` The API key is required for uploading, saving memories, and recalling the memory chain. It is optional for general file downloads — without it, the public gateway is used and files are returned as stored (i.e. compressed files will not be decompressed). ## Core Operations ### Upload a File ```bash scripts/autodrive-upload.sh <filepath> [--json] [--compress] ``` Uploads a file to Auto-Drive mainnet using the 3-step upload protocol (single chunk). Returns the CID on stdout. Requires `AUTO_DRIVE_API_KEY`. - `--json` — force MIME type to `application/json` - `--compress` — enable ZLIB compression ### Download a File ```bash scripts/autodrive-download.sh <cid> [output_path] ``` Downloads a file by CID. Uses the authenticated API if `AUTO_DRIVE_API_KEY` is set (decompresses server-side), otherwise uses the public gateway (files returned as stored). If `output_path` is omitted, outputs to stdout. ### Save a Memory Entry ```bash scripts/autodrive-save-memory.sh <data_file_or_string> [--agent-name NAME] [--state-file PATH] ``` Creates a memory experience with the Autonomys Agents header/data structure: ```json { "header": { "agentName": "my-agent", "agentVersion": "1.0.0", "timestamp": "2026-02-14T00:00:00.000Z", "previousCid": "bafk...or null" }, "data": { "type": "memory", "content": "..." } } ``` - If the first argument is a **file path**, its JSON contents become the `data` payload. - If the first argument is a **plain string**, it is wrapped as `{"type": "memory", "content": "..."}`. - `--agent-name` — set the agent name in the header (default: `openclaw-agent` or `$AGENT_NAME`) - `--state-file` — override the state file location Uploads to Auto-Drive and updates the state file with the new head CID. Also pins the latest CID to `MEMORY.md` if that file exists in the workspace. Returns structured JSON on stdout: ```json {"cid": "bafk...", "previousCid": "bafk...", "chainLength": 5} ``` ### Recall the Full Chain ```bash scripts/autodrive-recall-chain.sh [cid] [--limit N] [--output-dir DIR] ``` If no CID is given, reads the latest CID from the state file. Walks the linked list from newest to oldest, outputting each experience as JSON. - `--limit N` — maximum entries to retrieve (default: 50) - `--output-dir DIR` — save each entry as a numbered JSON file instead of printing to stdout Supports both `header.previousCid` (Autonomys Agents format) and root-level `previousCid` for backward compatibility. This is the **resurrection** mechanism: a new agent instance only needs one CID to rebuild its entire memory. ## The Resurrection Concept Every memory saved gets a unique CID and points back to the previous one: ``` Experience #3 (CID: bafk...xyz) → header.previousCid: bafk...def (Experience #2) → header.previousCid: bafk...abc (Experience #1) → header.previousCid: null (genesis) ``` If the agent's server dies, a new instance only needs the last CID to walk the entire chain and reconstruct full context. It's version control for consciousness, stored permanently on the Autonomys Network. ## Usage Examples **User:** "Upload my report to Auto-Drive" → Run `scripts/autodrive-upload.sh /path/to/report.pdf` → Report back the CID and gateway link **User:** "Upload with compression" → Run `scripts/autodrive-upload.sh /path/to/data.json --json --compress` **User:** "Save a memory that we decided to use React for the frontend" → Run `scripts/autodrive-save-memory.sh "Decision: using React for frontend. Reason: team familiarity and component reuse."` **User:** "Save a structured memory" → Create a JSON file, then run `scripts/autodrive-save-memory.sh /tmp/milestone.json --agent-name my-agent` **User:** "Resurrect my memory chain" → Run `scripts/autodrive-recall-chain.sh` → Display the full history from genesis to present **User:** "Download bafk...abc from Autonomys" → Run `scripts/autodrive-download.sh bafk...abc ./downloaded_file` ## Important Notes - All data stored on Auto-Drive is **permanent and public** by default. Do not store secrets, private keys, or sensitive personal data. - The free API key has a **20 MB per month upload limit** on mainnet. Downloads are unlimited. Check remaining credits via `GET /accounts/@me` or run `scripts/verify-setup.sh`. - An API key is required for uploads, memory saves, and chain recall. General file downloads work without one via the public gateway, but compressed files will not be decompressed. - The memory state file tracks `lastCid`, `lastUploadTimestamp`, and `chainLength`. Back up the `lastCid` value — it's your resurrection key. - The `autodrive-save-memory.sh` script **automatically pins the latest CID to `MEMORY.md`** if the file exists in the workspace. It creates an `## Auto-Drive Chain` section and updates it on each save. You do not need to track the latest CID in MEMORY.md manually — the script handles this. - Files are uploaded in a single chunk. The free tier's 20 MB/month limit is effectively a per-file ceiling — keep individual uploads well under that to preserve your monthly budget. - Gateway URL for any file: `https://gateway.autonomys.xyz/file/<CID>` - For true resurrection resilience, consider anchoring the latest CID on-chain via the Autonomys EVM — this makes recovery possible without keeping track of the head CID yourself. See [openclaw-memory-chain](https://github.com/autojeremy/openclaw-memory-chain) for an example contract implementation.

path: scripts/verify-setup.sh
content: #!/usr/bin/env bash
# Verify Auto-Drive setup — checks API key, account info, and remaining credits
# Usage: ./scripts/verify-setup.sh

set -euo pipefail

# shellcheck source=_lib.sh
source "$(dirname "$0")/_lib.sh"

ad_warn_git_bash

echo ""
echo "=== Auto-Drive Setup Verification ==="
echo ""

# Check prerequisites first — curl and jq are needed for verification below
MISSING=()
for bin in curl jq file; do
  if command -v "$bin" &>/dev/null; then
    echo -e "  ${GREEN}✓ $bin${NC}"
  else
    echo -e "  ${RED}✗ $bin not found${NC}"
    MISSING+=("$bin")
  fi
done

if [[ ${#MISSING[@]} -gt 0 ]]; then
  echo -e "${RED}✗ Missing prerequisites: ${MISSING[*]}${NC}" >&2
  exit 1
fi
echo ""

# Check for API key
if [[ -z "${AUTO_DRIVE_API_KEY:-}" ]]; then
  echo -e "${RED}✗ AUTO_DRIVE_API_KEY is not set${NC}" >&2
  echo "  Run: scripts/setup-auto-drive.sh" >&2
  exit 1
fi
echo -e "${GREEN}✓ AUTO_DRIVE_API_KEY is set${NC}"

# Verify key and fetch account info
if ! autodrive_verify_key "$AUTO_DRIVE_API_KEY"; then
  echo "  The key may be invalid or expired. Run: scripts/update-api-key.sh" >&2
  exit 1
fi

# Display account details (AD_VERIFY_BODY set by autodrive_verify_key)
LIMIT=$(echo "$AD_VERIFY_BODY" | jq -r '.uploadLimit // .limits.uploadLimit // empty' 2>/dev/null || true)
USED=$(echo "$AD_VERIFY_BODY" | jq -r '.uploadedBytes // .limits.uploadedBytes // empty' 2>/dev/null || true)

if [[ -n "$LIMIT" && -n "$USED" ]]; then
  REMAINING=$((LIMIT - USED))
  if command -v bc &>/dev/null; then
    UNIT="MB"
    LIMIT_FMT=$(echo "scale=1; $LIMIT / 1048576" | bc)
    USED_FMT=$(echo "scale=1; $USED / 1048576" | bc)
    REMAINING_FMT=$(echo "scale=1; $REMAINING / 1048576" | bc)
  else
    UNIT="bytes"
    LIMIT_FMT="$LIMIT"
    USED_FMT="$USED"
    REMAINING_FMT="$REMAINING"
  fi
  echo "  Upload limit:    ${LIMIT_FMT} ${UNIT} / month"
  echo "  Used this month: ${USED_FMT} ${UNIT}"
  if [[ "$REMAINING" -lt 1048576 ]]; then
    echo -e "  Remaining:       ${YELLOW}${REMAINING_FMT} ${UNIT} (low)${NC}"
  else
    echo -e "  Remaining:       ${GREEN}${REMAINING_FMT} ${UNIT}${NC}"
  fi
fi

echo ""
echo -e "${GREEN}All checks passed. Auto-Drive is ready.${NC}"
echo ""
echo "Try: scripts/autodrive-upload.sh /path/to/any/file"
echo ""


path: scripts/autodrive-upload.sh
content: #!/usr/bin/env bash
# Upload a file or JSON object to Auto-Drive (3-step chunked upload)
# Usage: autodrive-upload.sh <file_path> [--json] [--compress]
# Env: AUTO_DRIVE_API_KEY (required)
# Output: CID on success (stdout), status messages on stderr

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./_lib.sh
source "$SCRIPT_DIR/_lib.sh"
ad_warn_git_bash
ad_require_tools curl jq

FILE_PATH="${1:?Usage: autodrive-upload.sh <file_path> [--json] [--compress]}"
IS_JSON=false
COMPRESS=false
shift
for arg in "$@"; do
  case "$arg" in
    --json) IS_JSON=true ;;
    --compress) COMPRESS=true ;;
  esac
done

if [[ -z "${AUTO_DRIVE_API_KEY:-}" ]]; then
  echo "Error: AUTO_DRIVE_API_KEY not set." >&2
  echo "Get a free key at https://ai3.storage (sign in with Google/GitHub → Developers → Create API Key)" >&2
  exit 1
fi

if [[ ! -f "$FILE_PATH" ]]; then
  echo "Error: File not found: $FILE_PATH" >&2
  exit 1
fi

FILENAME=$(basename "$FILE_PATH")
if $IS_JSON; then
  MIME="application/json"
else
  if command -v file &>/dev/null; then
    MIME=$(file --mime-type -b "$FILE_PATH" 2>/dev/null || echo "application/octet-stream")
  else
    case "${FILE_PATH##*.}" in
      json)     MIME="application/json" ;;
      txt)      MIME="text/plain" ;;
      pdf)      MIME="application/pdf" ;;
      png)      MIME="image/png" ;;
      jpg|jpeg) MIME="image/jpeg" ;;
      *)        MIME="application/octet-stream" ;;
    esac
  fi
fi

AUTH_HEADERS=(-H "Authorization: Bearer $AUTO_DRIVE_API_KEY" -H "X-Auth-Provider: apikey")

# Step 1: Create upload
echo "Creating upload for '$FILENAME'..." >&2
if $COMPRESS; then
  UPLOAD_BODY=$(jq -n --arg fn "$FILENAME" --arg mt "$MIME" \
    '{filename: $fn, mimeType: $mt, uploadOptions: {compression: {algorithm: "ZLIB"}}}')
else
  UPLOAD_BODY=$(jq -n --arg fn "$FILENAME" --arg mt "$MIME" \
    '{filename: $fn, mimeType: $mt, uploadOptions: {}}')
fi

RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "$AD_API_BASE/uploads/file" \
  "${AUTH_HEADERS[@]}" \
  -H "Content-Type: application/json" \
  -d "$UPLOAD_BODY")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
  echo "Error: Failed to create upload (HTTP $HTTP_CODE)" >&2
  echo "$BODY" >&2
  exit 1
fi

UPLOAD_ID=$(echo "$BODY" | jq -r '.id')
if [[ -z "$UPLOAD_ID" || "$UPLOAD_ID" == "null" ]]; then
  echo "Error: Failed to create upload — no upload ID returned" >&2
  echo "$BODY" >&2
  exit 1
fi

# Step 2: Upload chunk
echo "Uploading file data..." >&2
RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "$AD_API_BASE/uploads/file/$UPLOAD_ID/chunk" \
  "${AUTH_HEADERS[@]}" \
  -F "file=@$FILE_PATH" \
  -F "index=0")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
  BODY=$(echo "$RESPONSE" | sed '$d')
  echo "Error: Failed to upload chunk (HTTP $HTTP_CODE)" >&2
  echo "$BODY" >&2
  exit 1
fi

# Step 3: Complete upload → get CID
echo "Completing upload..." >&2
RESPONSE=$(curl -sS -w "\n%{http_code}" -X POST "$AD_API_BASE/uploads/$UPLOAD_ID/complete" \
  "${AUTH_HEADERS[@]}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
  echo "Error: Failed to complete upload (HTTP $HTTP_CODE)" >&2
  echo "$BODY" >&2
  exit 1
fi

CID=$(echo "$BODY" | jq -r '.cid')
if [[ -z "$CID" || "$CID" == "null" ]]; then
  echo "Error: Upload completed but no CID returned" >&2
  echo "$BODY" >&2
  exit 1
fi

# Validate CID format to prevent command injection and chain corruption
if ! ad_valid_cid "$CID"; then
  echo "Error: Invalid CID format returned: $CID" >&2
  exit 1
fi

echo "Upload successful! CID: $CID" >&2
echo "Gateway URL: https://gateway.autonomys.xyz/file/$CID" >&2
echo "$CID"


path: scripts/autodrive-download.sh
content: #!/usr/bin/env bash
# Download a file from Auto-Drive by CID
# Usage: autodrive-download.sh <cid> [output_path]
# Tries the public download API first (handles server-side decompression); falls back to
# the public gateway if the API fails. Auth headers are sent when AUTO_DRIVE_API_KEY is set.
# If output_path is omitted, outputs to stdout.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./_lib.sh
source "$SCRIPT_DIR/_lib.sh"
ad_warn_git_bash
ad_require_tools curl

CID="${1:?Usage: autodrive-download.sh <cid> [output_path]}"
OUTPUT="${2:-}"

# Validate CID format
if ! ad_valid_cid "$CID"; then
  echo "Error: Invalid CID format: $CID" >&2
  exit 1
fi

# Validate output path: reject traversal, resolve physically, verify within $HOME.
if [[ -n "$OUTPUT" ]]; then
  if [[ "$OUTPUT" == *..* ]]; then
    echo "Error: Output path must not contain '..': $OUTPUT" >&2; exit 1
  fi
  OUTPUT_DIR_PART="$(dirname "$OUTPUT")"
  OUTPUT_BASE="$(basename "$OUTPUT")"
  OUTPUT_RESOLVED="$(cd "$OUTPUT_DIR_PART" 2>/dev/null && pwd -P)/$OUTPUT_BASE" || {
    echo "Error: Could not resolve output path — directory does not exist: $OUTPUT_DIR_PART" >&2; exit 1
  }
  HOME_REAL="$(cd "$HOME" && pwd -P)"
  if [[ "$OUTPUT_RESOLVED" != "$HOME_REAL/"* ]]; then
    echo "Error: Output path must be within home directory" >&2
    exit 1
  fi
  OUTPUT="$OUTPUT_RESOLVED"
fi

GATEWAY="https://gateway.autonomys.xyz"

download_to_file() {
  local URL="$1" DEST="$2"
  shift 2
  RESPONSE=$(curl -sS -w "\n%{http_code}" "$URL" "$@" -o "$DEST")
  echo "$RESPONSE" | tail -1
}

# Send auth headers when API key is available (gives user-level access);
# the download API is public either way and handles server-side decompression.
AUTH_ARGS=()
if [[ -n "${AUTO_DRIVE_API_KEY:-}" ]]; then
  AUTH_ARGS=(-H "Authorization: Bearer $AUTO_DRIVE_API_KEY" -H "X-Auth-Provider: apikey")
fi

if [[ -z "$OUTPUT" ]]; then
  # Output to stdout — try download API first, fall back to gateway
  curl -sS --fail "$AD_DOWNLOAD_API/downloads/$CID" "${AUTH_ARGS[@]}" 2>/dev/null \
    || curl -sS --fail "$GATEWAY/file/$CID"
else
  # Output to file — check HTTP codes for proper error reporting
  HTTP_CODE=$(download_to_file "$AD_DOWNLOAD_API/downloads/$CID" "$OUTPUT" "${AUTH_ARGS[@]}")
  if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
    echo "Saved to: $OUTPUT" >&2
  else
    echo "Error: API download failed (HTTP $HTTP_CODE) — trying gateway" >&2
    HTTP_CODE=$(download_to_file "$GATEWAY/file/$CID" "$OUTPUT")
    if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
      echo "Error: Gateway download also failed (HTTP $HTTP_CODE)" >&2
      rm -f "$OUTPUT"
      exit 1
    fi
    echo "Saved to: $OUTPUT (via public gateway — file may be in compressed form)" >&2
  fi
fi


path: scripts/autodrive-save-memory.sh
content: #!/usr/bin/env bash
# Save a memory experience to Auto-Drive as part of the linked list chain
# Usage: autodrive-save-memory.sh <data_file_or_string> [--agent-name NAME] [--state-file PATH]
# Env: AUTO_DRIVE_API_KEY (required), AGENT_NAME (optional, default: openclaw-agent),
#      OPENCLAW_WORKSPACE (optional, default: $HOME/.openclaw/workspace)
# Output: JSON with cid, previousCid, chainLength (stdout)
#
# If the first argument is a file path, its contents are used as the data payload.
# If the first argument is a plain string, it is wrapped as {"type":"memory","content":"..."}.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./_lib.sh
source "$SCRIPT_DIR/_lib.sh"
ad_warn_git_bash
ad_require_tools curl jq
INPUT="${1:?Usage: autodrive-save-memory.sh <data_file_or_string> [--agent-name NAME] [--state-file PATH]}"
AGENT_NAME="${AGENT_NAME:-openclaw-agent}"
STATE_FILE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}/memory/autodrive-state.json"
STATE_FILE_EXPLICIT=false

shift
while [[ $# -gt 0 ]]; do
  case "$1" in
    --agent-name)
      if [[ $# -lt 2 ]]; then echo "Error: --agent-name requires a value" >&2; exit 1; fi
      AGENT_NAME="$2"; shift 2 ;;
    --state-file)
      if [[ $# -lt 2 ]]; then echo "Error: --state-file requires a value" >&2; exit 1; fi
      STATE_FILE="$2"; STATE_FILE_EXPLICIT=true; shift 2 ;;
    *) shift ;;
  esac
done

# Validate explicit --state-file: reject traversal, resolve physically, verify within $HOME.
# Skipped for the default path which derives from a trusted env var.
if [[ "$STATE_FILE_EXPLICIT" == true ]]; then
  if [[ "$STATE_FILE" == *..* ]]; then
    echo "Error: State file path must not contain '..': $STATE_FILE" >&2; exit 1
  fi
  HOME_REAL="$(cd "$HOME" && pwd -P)"
  # Convert to absolute, normalize $HOME to physical path.
  if [[ "$STATE_FILE" != /* ]]; then
    STATE_FILE_CHECK="$(pwd -P)/$STATE_FILE"
  else
    STATE_FILE_CHECK="$STATE_FILE"
  fi
  if [[ "$STATE_FILE_CHECK" == "$HOME/"* ]]; then
    STATE_FILE_CHECK="$HOME_REAL/${STATE_FILE_CHECK#"$HOME/"}"
  elif [[ "$STATE_FILE_CHECK" == "$HOME" ]]; then
    STATE_FILE_CHECK="$HOME_REAL"
  fi
  # Resolve parent physically if it exists (catches symlinks).
  STATE_FILE_DIR="$(dirname "$STATE_FILE_CHECK")"
  STATE_FILE_BASE="$(basename "$STATE_FILE_CHECK")"
  if [[ -d "$STATE_FILE_DIR" ]]; then
    STATE_FILE_CHECK="$(cd "$STATE_FILE_DIR" && pwd -P)/$STATE_FILE_BASE"
  fi
  if [[ "$STATE_FILE_CHECK" != "$HOME_REAL/"* ]]; then
    echo "Error: State file path must be within home directory" >&2
    exit 1
  fi
  STATE_FILE="$STATE_FILE_CHECK"
fi

if [[ -z "${AUTO_DRIVE_API_KEY:-}" ]]; then
  echo "Error: AUTO_DRIVE_API_KEY not set." >&2
  echo "Get a free key at https://ai3.storage (sign in with Google/GitHub → Developers → Create API Key)" >&2
  exit 1
fi

# Determine if input is a file or a string
if [[ -f "$INPUT" ]]; then
  # Validate it's JSON
  if ! jq empty "$INPUT" 2>/dev/null; then
    echo "Error: Data file is not valid JSON: $INPUT" >&2
    exit 1
  fi
  DATA_JSON=$(cat "$INPUT")
else
  # Wrap the plain string as a simple memory object
  DATA_JSON=$(jq -n --arg content "$INPUT" '{type: "memory", content: $content}')
fi

# Read previous CID and chain length from state file
PREVIOUS_CID="null"
CHAIN_LENGTH=0
if [[ -f "$STATE_FILE" ]]; then
  PREVIOUS_CID=$(jq -r '.lastCid // empty' "$STATE_FILE" 2>/dev/null || echo "null")
  CHAIN_LENGTH=$(jq -r '.chainLength // 0' "$STATE_FILE" 2>/dev/null || echo "0")
  [[ -z "$PREVIOUS_CID" ]] && PREVIOUS_CID="null"
  # Reject a corrupted/tampered state file CID rather than propagating it into the chain
  if [[ "$PREVIOUS_CID" != "null" ]] && ! ad_valid_cid "$PREVIOUS_CID"; then
    echo "Warning: State file contains invalid CID '$PREVIOUS_CID' — starting new chain" >&2
    PREVIOUS_CID="null"
    CHAIN_LENGTH=0
  fi
fi

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")

# Build the experience JSON with header/data structure
EXPERIENCE=$(jq -n \
  --arg name "$AGENT_NAME" \
  --arg ts "$TIMESTAMP" \
  --arg prev "$PREVIOUS_CID" \
  --argjson data "$DATA_JSON" \
  '{
    header: {
      agentName: $name,
      agentVersion: "1.0.0",
      timestamp: $ts,
      previousCid: (if $prev == "null" then null else $prev end)
    },
    data: $data
  }')

# Write to temp file and upload via the upload script
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "$EXPERIENCE" > "$TMPFILE"
CID=$("$SCRIPT_DIR/autodrive-upload.sh" "$TMPFILE" --json --compress)

if [[ -z "$CID" ]]; then
  echo "Error: Upload failed — no CID returned" >&2
  exit 1
fi

# Validate CID format
if ! ad_valid_cid "$CID"; then
  echo "Error: Invalid CID format returned: $CID" >&2
  exit 1
fi

# Update state file
NEW_LENGTH=$((CHAIN_LENGTH + 1))
mkdir -p "$(dirname "$STATE_FILE")"
jq -n \
  --arg cid "$CID" \
  --arg ts "$TIMESTAMP" \
  --argjson len "$NEW_LENGTH" \
  '{lastCid: $cid, lastUploadTimestamp: $ts, chainLength: $len}' > "$STATE_FILE"
chmod 600 "$STATE_FILE"

# Pin latest CID to MEMORY.md for session continuity (if it exists)
MEMORY_FILE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}/MEMORY.md"
if [[ -f "$MEMORY_FILE" ]]; then
  if grep -q "^## Auto-Drive Chain" "$MEMORY_FILE"; then
    if grep -q "^\- \*\*Latest CID:\*\*" "$MEMORY_FILE"; then
      # Portable sed -i (works on both macOS and Linux)
      SEDTMP=$(mktemp "${MEMORY_FILE}.XXXXXX")
      sed "s|^- \*\*Latest CID:\*\*.*|- **Latest CID:** \`$CID\` (chain length: $NEW_LENGTH, updated: $TIMESTAMP)|" "$MEMORY_FILE" > "$SEDTMP" && mv "$SEDTMP" "$MEMORY_FILE"
    else
      printf '- **Latest CID:** `%s` (chain length: %d, updated: %s)\n' "$CID" "$NEW_LENGTH" "$TIMESTAMP" >> "$MEMORY_FILE"
    fi
  else
    printf '\n## Auto-Drive Chain\n- **Latest CID:** `%s` (chain length: %d, updated: %s)\n' "$CID" "$NEW_LENGTH" "$TIMESTAMP" >> "$MEMORY_FILE"
  fi
fi

# Output structured result
jq -n \
  --arg cid "$CID" \
  --arg prev "$PREVIOUS_CID" \
  --argjson len "$NEW_LENGTH" \
  '{cid: $cid, previousCid: (if $prev == "null" then null else $prev end), chainLength: $len}'


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)