wbxcallprov/docker/remote-agent/inspect-bundle.sh
jmcqueen 93b060bc8b Modernize bot to Node.js 22 with modular architecture and remote SIW agent
- Refactor monolithic index.js (2646 lines) into src/{webex,integrations,
  cards,flows,commands,services} modules; replace node-fetch/form-data
  with native fetch/FormData; move all secrets to .env via dotenv
- Add dockerized remote SIW agent (docker/remote-agent/) with cross-arch
  buildx packaging (arm64 Mac -> linux/amd64), idempotent install.sh
  deploy bundle, and docker-free ZIP inspector for arch verification
- Bot hosts a WebSocket server; agent proxies SIW requests with a
  per-request insecure:true flag, replacing the process-wide
  NODE_TLS_REJECT_UNAUTHORIZED bypass
- Add ESLint flat config + Prettier, rewrite Dockerfile as non-root
  multi-stage node:22-alpine build, README covering setup / deploy /
  remote agent workflow
- Fix parseStoreArg to read trigger.prompt correctly (was indexing past
  the framework's post-match slice); register /help as regex (string
  matcher only compares the first token); switch catch-all to /.+/
  (previous /.*/gim was stateful due to the g flag); remove
  /fixDisplayNames command and its flow/card

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 14:48:51 -04:00

135 lines
4.8 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Inspect a wbxprov-remote-agent deploy ZIP (or the tarball inside one) and
# report the actual OS/architecture of the Docker image it contains — WITHOUT
# needing docker installed. Reads the OCI/Docker image manifest directly out
# of the tarball.
#
# Use this before shipping a ZIP to a remote host to confirm you built the
# right platform. The main `package.sh` already runs `docker image inspect`
# on the image it just built, but this script gives you a completely
# independent check (no docker daemon involved at all) that also works on
# machines that never had docker on them.
#
# Usage:
# ./docker/remote-agent/inspect-bundle.sh <path-to-zip-or-tar.gz>
# ./docker/remote-agent/inspect-bundle.sh # auto-detect newest ZIP in dist/
#
# Exit codes:
# 0 bundle looks well-formed; arch/os printed
# 1 bad arguments / no bundle found
# 2 bundle is malformed or arch could not be read
#
# Optional env:
# EXPECTED_ARCH=amd64 die if the image's architecture does not match
# EXPECTED_OS=linux die if the image's OS does not match
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DIST_DIR="$SCRIPT_DIR/dist"
RED=$'\033[0;31m'
GRN=$'\033[0;32m'
YLW=$'\033[1;33m'
RST=$'\033[0m'
log() { printf '%s[inspect]%s %s\n' "$GRN" "$RST" "$*"; }
warn() { printf '%s[inspect]%s %s\n' "$YLW" "$RST" "$*"; }
die() { printf '%s[inspect]%s %s\n' "$RED" "$RST" "$*" >&2; exit 2; }
command -v python3 >/dev/null 2>&1 || die "python3 is required (ships with macOS; on Linux: 'apt install python3')."
BUNDLE="${1:-}"
if [[ -z "$BUNDLE" ]]; then
# Auto-detect: newest ZIP in dist/
[[ -d "$DIST_DIR" ]] || { printf '%s\n' "usage: $0 <zip-or-tarball>"; exit 1; }
BUNDLE="$(ls -t "$DIST_DIR"/*.zip 2>/dev/null | head -n 1 || true)"
[[ -n "$BUNDLE" ]] || { printf '%s\n' "usage: $0 <zip-or-tarball> (no ZIPs in $DIST_DIR)"; exit 1; }
log "Auto-detected newest bundle: $BUNDLE"
fi
[[ -e "$BUNDLE" ]] || die "File not found: $BUNDLE"
# Resolve to the actual image tarball. If given a ZIP, extract to a tempdir
# and locate the tar.gz inside; otherwise assume the arg IS the tarball.
TMPDIR_INSPECT=""
cleanup() { [[ -n "$TMPDIR_INSPECT" ]] && rm -rf "$TMPDIR_INSPECT"; }
trap cleanup EXIT
case "$BUNDLE" in
*.zip)
command -v unzip >/dev/null 2>&1 || die "unzip is required to inspect a ZIP."
TMPDIR_INSPECT="$(mktemp -d)"
log "Extracting ZIP into temp dir for inspection..."
unzip -q "$BUNDLE" -d "$TMPDIR_INSPECT"
TARBALL="$(find "$TMPDIR_INSPECT" -maxdepth 3 -name '*.tar.gz' -type f | head -n 1 || true)"
[[ -n "$TARBALL" ]] || die "Could not find an image tarball (*.tar.gz) inside the ZIP."
;;
*.tar.gz|*.tgz|*.tar)
TARBALL="$BUNDLE"
;;
*)
die "Unrecognized bundle type: $BUNDLE (expected .zip, .tar.gz, .tgz, or .tar)"
;;
esac
log "Reading image manifest from: $(basename "$TARBALL")"
# The image tarball is a standard docker/OCI save. It contains:
# manifest.json -> lists image config path (per-tag entry)
# <config-sha>.json OR blobs/sha256/<sha> -> per-image config JSON
# We read manifest.json to find the config blob, then read the config
# blob's 'architecture' and 'os' fields. Everything happens in-memory via
# `tar -xO`, so nothing is written to disk.
DECOMPRESS="cat"
case "$TARBALL" in
*.gz|*.tgz) DECOMPRESS="gunzip -c" ;;
esac
MANIFEST_JSON="$($DECOMPRESS "$TARBALL" | tar -xO manifest.json 2>/dev/null || true)"
[[ -n "$MANIFEST_JSON" ]] || die "No manifest.json in tarball — is this really a 'docker save' bundle?"
CONFIG_PATH="$(printf '%s' "$MANIFEST_JSON" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data:
sys.exit('empty manifest')
entry = data[0]
config = entry.get('Config') or entry.get('config')
if not config:
sys.exit('no Config in manifest entry')
print(config)
")"
CONFIG_JSON="$($DECOMPRESS "$TARBALL" | tar -xO "$CONFIG_PATH" 2>/dev/null || true)"
[[ -n "$CONFIG_JSON" ]] || die "Could not read config blob '$CONFIG_PATH' from tarball."
read -r ARCH OS <<<"$(printf '%s' "$CONFIG_JSON" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('architecture','?'), d.get('os','?'))
")"
log "Image tags: $(printf '%s' "$MANIFEST_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(', '.join(d[0].get('RepoTags') or ['(none)']))")"
log "Image OS: ${OS}"
log "Image arch: ${ARCH}"
FAIL=0
if [[ -n "${EXPECTED_ARCH:-}" && "${ARCH}" != "${EXPECTED_ARCH}" ]]; then
warn "Expected arch '${EXPECTED_ARCH}' but got '${ARCH}'."
FAIL=1
fi
if [[ -n "${EXPECTED_OS:-}" && "${OS}" != "${EXPECTED_OS}" ]]; then
warn "Expected OS '${EXPECTED_OS}' but got '${OS}'."
FAIL=1
fi
if (( FAIL != 0 )); then
die "Bundle does not match expected OS/arch. Rebuild with the correct --platform."
fi
log "OK — bundle looks well-formed."