|
| 1 | +#!/usr/bin/env bash |
| 2 | +# demo-credentials.sh — acpctl credential lifecycle demo |
| 3 | +# |
| 4 | +# Demonstrates the full credential workflow: |
| 5 | +# 1. Verify login |
| 6 | +# 2. Create three credentials (GitHub, GitLab, Jira) |
| 7 | +# 3. Create a project |
| 8 | +# 4. Bind all credentials to the project |
| 9 | +# 5. Create an agent and start a session |
| 10 | +# 6. Ask the agent to verify it can access the credentials |
| 11 | +# 7. Clean up |
| 12 | +# |
| 13 | +# Prerequisites: |
| 14 | +# You must create three secret files before running this demo: |
| 15 | +# |
| 16 | +# .secrets/GITHUB_TOKEN — a GitHub Personal Access Token (classic or fine-grained) |
| 17 | +# Create at: https://github.com/settings/tokens |
| 18 | +# Required scopes: repo (or fine-grained with Contents read) |
| 19 | +# |
| 20 | +# .secrets/GITLAB_TOKEN — a GitLab Personal Access Token |
| 21 | +# Create at: https://gitlab.com/-/user_settings/personal_access_tokens |
| 22 | +# Required scopes: read_api |
| 23 | +# |
| 24 | +# .secrets/JIRA_TOKEN — a Jira API Token |
| 25 | +# Create at: https://id.atlassian.com/manage-profile/security/api-tokens |
| 26 | +# Used with your Atlassian email for Basic auth |
| 27 | +# |
| 28 | +# Each file should contain the raw token string with no trailing newline. |
| 29 | +# Example: |
| 30 | +# echo -n "ghp_abc123..." > .secrets/GITHUB_TOKEN |
| 31 | +# echo -n "glpat-xyz..." > .secrets/GITLAB_TOKEN |
| 32 | +# echo -n "ATATT3x..." > .secrets/JIRA_TOKEN |
| 33 | +# chmod 600 .secrets/* |
| 34 | +# |
| 35 | +# Usage: |
| 36 | +# ./demo-credentials.sh |
| 37 | +# PAUSE=2 ./demo-credentials.sh # pause between steps |
| 38 | +# SECRETS_DIR=~/my-secrets ./demo-credentials.sh |
| 39 | +# NO_CLEANUP=1 ./demo-credentials.sh # skip cleanup |
| 40 | +# |
| 41 | +# Optional env: |
| 42 | +# SECRETS_DIR — directory containing token files (default: .secrets) |
| 43 | +# JIRA_URL — Jira instance URL (default: prompted) |
| 44 | +# JIRA_EMAIL — Jira account email (default: prompted) |
| 45 | +# GITLAB_URL — GitLab instance URL (default: https://gitlab.com) |
| 46 | +# ACPCTL — path to acpctl binary (default: acpctl from PATH) |
| 47 | +# PAUSE — seconds between demo steps (default: 0) |
| 48 | +# SESSION_READY_TIMEOUT — seconds to wait for Running (default: 180) |
| 49 | +# MESSAGE_WAIT_TIMEOUT — seconds to wait for RUN_FINISHED (default: 300) |
| 50 | +# NO_CLEANUP — set to 1 to skip cleanup |
| 51 | + |
| 52 | +set -euo pipefail |
| 53 | + |
| 54 | +ACPCTL="${ACPCTL:-acpctl}" |
| 55 | +PAUSE="${PAUSE:-0}" |
| 56 | +SESSION_READY_TIMEOUT="${SESSION_READY_TIMEOUT:-180}" |
| 57 | +MESSAGE_WAIT_TIMEOUT="${MESSAGE_WAIT_TIMEOUT:-300}" |
| 58 | +SECRETS_DIR="${SECRETS_DIR:-.secrets}" |
| 59 | +GITLAB_URL="${GITLAB_URL:-https://gitlab.com}" |
| 60 | + |
| 61 | +# ── helpers ──────────────────────────────────────────────────────────────────── |
| 62 | + |
| 63 | +bold() { printf '\033[1m%s\033[0m\n' "$*"; } |
| 64 | +dim() { printf '\033[2m%s\033[0m\n' "$*"; } |
| 65 | +cyan() { printf '\033[36m%s\033[0m\n' "$*"; } |
| 66 | +green() { printf '\033[32m%s\033[0m\n' "$*"; } |
| 67 | +yellow(){ printf '\033[33m%s\033[0m\n' "$*"; } |
| 68 | +red() { printf '\033[31m%s\033[0m\n' "$*"; } |
| 69 | +sep() { printf '\033[2m%s\033[0m\n' "──────────────────────────────────────────────────"; } |
| 70 | + |
| 71 | +step() { |
| 72 | + local description="$1" |
| 73 | + shift |
| 74 | + echo |
| 75 | + sep |
| 76 | + bold "▶ $description" |
| 77 | + printf '\033[38;5;214m $ %s\033[0m\n' "$*" |
| 78 | + sleep "$PAUSE" |
| 79 | + "$@" |
| 80 | + echo |
| 81 | +} |
| 82 | + |
| 83 | +announce() { |
| 84 | + echo |
| 85 | + sep |
| 86 | + cyan "━━ $*" |
| 87 | + sep |
| 88 | + sleep "$PAUSE" |
| 89 | +} |
| 90 | + |
| 91 | +die() { red "error: $*" >&2; exit 1; } |
| 92 | + |
| 93 | +json_field() { |
| 94 | + local json="$1" field="$2" |
| 95 | + echo "$json" | python3 -c "import sys,json; print(json.load(sys.stdin)['${field}'])" 2>/dev/null |
| 96 | +} |
| 97 | + |
| 98 | +wait_for_running() { |
| 99 | + local session_id="$1" |
| 100 | + local deadline=$(( $(date +%s) + SESSION_READY_TIMEOUT )) |
| 101 | + local last_phase="" |
| 102 | + printf ' waiting for Running (timeout %ds)...\n' "${SESSION_READY_TIMEOUT}" |
| 103 | + while true; do |
| 104 | + local phase |
| 105 | + phase=$( |
| 106 | + "$ACPCTL" get session "$session_id" -o json 2>/dev/null \ |
| 107 | + | python3 -c "import sys,json; print(json.load(sys.stdin).get('phase',''))" 2>/dev/null || true |
| 108 | + ) |
| 109 | + if [[ "$phase" != "$last_phase" ]]; then |
| 110 | + printf ' phase: %s\n' "$phase" |
| 111 | + last_phase="$phase" |
| 112 | + fi |
| 113 | + [[ "$phase" == "Running" ]] && { green " session is Running"; return 0; } |
| 114 | + [[ $(date +%s) -ge $deadline ]] && { yellow " timed out (phase=${phase:-unknown})"; return 1; } |
| 115 | + sleep 3 |
| 116 | + done |
| 117 | +} |
| 118 | + |
| 119 | +# ── preflight ────────────────────────────────────────────────────────────────── |
| 120 | + |
| 121 | +command -v "$ACPCTL" &>/dev/null || die "${ACPCTL} not found. Set ACPCTL=/path/to/acpctl or add to PATH." |
| 122 | +command -v python3 &>/dev/null || die "python3 not found." |
| 123 | + |
| 124 | +# ── intro ──────────────────────────────────────────────────────────────────── |
| 125 | + |
| 126 | +echo |
| 127 | +bold "Ambient CLI Demo — Credential Lifecycle" |
| 128 | +sep |
| 129 | +echo |
| 130 | +printf ' %s\n' "This demo creates three credentials (GitHub, GitLab, Jira)," |
| 131 | +printf ' %s\n' "binds them to a project, starts an agent session, and verifies" |
| 132 | +printf ' %s\n' "the agent can access all three credentials at runtime." |
| 133 | +echo |
| 134 | +printf ' %s\n' "Steps:" |
| 135 | +printf ' %s\n' " 1. Verify login" |
| 136 | +printf ' %s\n' " 2. Create credentials: github-pat, gitlab-pat, jira-token" |
| 137 | +printf ' %s\n' " 3. Create project" |
| 138 | +printf ' %s\n' " 4. Bind all credentials to the project" |
| 139 | +printf ' %s\n' " 5. Create agent + start session" |
| 140 | +printf ' %s\n' " 6. Verify credentials in session" |
| 141 | +printf ' %s\n' " 7. Clean up" |
| 142 | +echo |
| 143 | +printf ' \033[38;5;214m%-38s\033[0m %s\n' "Orange text like this" "= a terminal command being run" |
| 144 | +echo |
| 145 | +sep |
| 146 | +echo |
| 147 | +bold " Prerequisites — secret files" |
| 148 | +echo |
| 149 | +printf ' %s\n' "The demo reads tokens from files in ${SECRETS_DIR}/:" |
| 150 | +echo |
| 151 | +printf ' \033[36m%-28s\033[0m %s\n' "${SECRETS_DIR}/GITHUB_TOKEN" "GitHub PAT (https://github.com/settings/tokens)" |
| 152 | +printf ' \033[36m%-28s\033[0m %s\n' "${SECRETS_DIR}/GITLAB_TOKEN" "GitLab PAT (https://gitlab.com/-/user_settings/personal_access_tokens)" |
| 153 | +printf ' \033[36m%-28s\033[0m %s\n' "${SECRETS_DIR}/JIRA_TOKEN" "Jira token (https://id.atlassian.com/manage-profile/security/api-tokens)" |
| 154 | +echo |
| 155 | +printf ' %s\n' "Create them like this:" |
| 156 | +dim " echo -n \"ghp_abc123...\" > ${SECRETS_DIR}/GITHUB_TOKEN" |
| 157 | +dim " echo -n \"glpat-xyz...\" > ${SECRETS_DIR}/GITLAB_TOKEN" |
| 158 | +dim " echo -n \"ATATT3x...\" > ${SECRETS_DIR}/JIRA_TOKEN" |
| 159 | +dim " chmod 600 ${SECRETS_DIR}/*" |
| 160 | +echo |
| 161 | +sep |
| 162 | + |
| 163 | +# ── validate secrets ───────────────────────────────────────────────────────── |
| 164 | + |
| 165 | +GITHUB_TOKEN_FILE="${SECRETS_DIR}/GITHUB_TOKEN" |
| 166 | +GITLAB_TOKEN_FILE="${SECRETS_DIR}/GITLAB_TOKEN" |
| 167 | +JIRA_TOKEN_FILE="${SECRETS_DIR}/JIRA_TOKEN" |
| 168 | + |
| 169 | +[[ -f "${GITHUB_TOKEN_FILE}" ]] || die "Missing ${GITHUB_TOKEN_FILE} — see prerequisites above." |
| 170 | +[[ -f "${GITLAB_TOKEN_FILE}" ]] || die "Missing ${GITLAB_TOKEN_FILE} — see prerequisites above." |
| 171 | +[[ -f "${JIRA_TOKEN_FILE}" ]] || die "Missing ${JIRA_TOKEN_FILE} — see prerequisites above." |
| 172 | + |
| 173 | +GITHUB_TOKEN_VALUE="$(cat "${GITHUB_TOKEN_FILE}")" |
| 174 | +GITLAB_TOKEN_VALUE="$(cat "${GITLAB_TOKEN_FILE}")" |
| 175 | +JIRA_TOKEN_VALUE="$(cat "${JIRA_TOKEN_FILE}")" |
| 176 | + |
| 177 | +[[ -n "${GITHUB_TOKEN_VALUE}" ]] || die "${GITHUB_TOKEN_FILE} is empty." |
| 178 | +[[ -n "${GITLAB_TOKEN_VALUE}" ]] || die "${GITLAB_TOKEN_FILE} is empty." |
| 179 | +[[ -n "${JIRA_TOKEN_VALUE}" ]] || die "${JIRA_TOKEN_FILE} is empty." |
| 180 | + |
| 181 | +green " All three secret files found." |
| 182 | + |
| 183 | +# ── gather Jira config ─────────────────────────────────────────────────────── |
| 184 | + |
| 185 | +if [[ -z "${JIRA_URL:-}" ]]; then |
| 186 | + printf '\n\033[1m Jira instance URL\033[0m (e.g. https://myco.atlassian.net): ' |
| 187 | + read -r JIRA_URL |
| 188 | + [[ -n "${JIRA_URL}" ]] || die "JIRA_URL is required for the jira credential." |
| 189 | +fi |
| 190 | + |
| 191 | +if [[ -z "${JIRA_EMAIL:-}" ]]; then |
| 192 | + printf '\033[1m Jira account email\033[0m: ' |
| 193 | + read -r JIRA_EMAIL |
| 194 | + [[ -n "${JIRA_EMAIL}" ]] || die "JIRA_EMAIL is required for the jira credential." |
| 195 | +fi |
| 196 | + |
| 197 | +# ── generate names ─────────────────────────────────────────────────────────── |
| 198 | + |
| 199 | +RUN_ID=$(date +%s | tail -c6) |
| 200 | +PROJECT_NAME="demo-creds-${RUN_ID}" |
| 201 | +AGENT_NAME="credential-verifier" |
| 202 | + |
| 203 | +CRED_GITHUB="github-pat-${RUN_ID}" |
| 204 | +CRED_GITLAB="gitlab-pat-${RUN_ID}" |
| 205 | +CRED_JIRA="jira-token-${RUN_ID}" |
| 206 | + |
| 207 | +echo |
| 208 | +dim " Run ID: ${RUN_ID}" |
| 209 | +dim " Project: ${PROJECT_NAME}" |
| 210 | +dim " Agent: ${AGENT_NAME}" |
| 211 | +dim " GitHub: ${CRED_GITHUB}" |
| 212 | +dim " GitLab: ${CRED_GITLAB}" |
| 213 | +dim " Jira: ${CRED_JIRA}" |
| 214 | + |
| 215 | +echo |
| 216 | +bold " Press Enter to begin..." |
| 217 | +read -r |
| 218 | + |
| 219 | +# ── cleanup trap ───────────────────────────────────────────────────────────── |
| 220 | + |
| 221 | +CREATED_PROJECT="" |
| 222 | +CREATED_SESSION_ID="" |
| 223 | + |
| 224 | +cleanup() { |
| 225 | + if [[ -n "${NO_CLEANUP:-}" ]]; then |
| 226 | + echo |
| 227 | + yellow " NO_CLEANUP set — skipping cleanup" |
| 228 | + dim " project: ${CREATED_PROJECT}" |
| 229 | + dim " session: ${CREATED_SESSION_ID}" |
| 230 | + dim " credentials: ${CRED_GITHUB}, ${CRED_GITLAB}, ${CRED_JIRA}" |
| 231 | + return |
| 232 | + fi |
| 233 | + echo |
| 234 | + announce "Cleanup" |
| 235 | + if [[ -n "${CREATED_SESSION_ID}" ]]; then |
| 236 | + dim " stopping session ${CREATED_SESSION_ID}..." |
| 237 | + "$ACPCTL" stop "${CREATED_SESSION_ID}" 2>/dev/null || true |
| 238 | + "$ACPCTL" delete session "${CREATED_SESSION_ID}" -y 2>/dev/null || true |
| 239 | + fi |
| 240 | + for cred in "${CRED_GITHUB}" "${CRED_GITLAB}" "${CRED_JIRA}"; do |
| 241 | + dim " deleting credential ${cred}..." |
| 242 | + "$ACPCTL" credential delete "${cred}" --confirm 2>/dev/null || true |
| 243 | + done |
| 244 | + if [[ -n "${CREATED_PROJECT}" ]]; then |
| 245 | + dim " deleting project ${CREATED_PROJECT}..." |
| 246 | + "$ACPCTL" delete project "${CREATED_PROJECT}" -y 2>/dev/null || true |
| 247 | + fi |
| 248 | + green " cleanup done" |
| 249 | +} |
| 250 | +trap cleanup EXIT |
| 251 | + |
| 252 | +# ── 1: verify login ───────────────────────────────────────────────────────── |
| 253 | + |
| 254 | +announce "1 · Verify login" |
| 255 | + |
| 256 | +step "Show authenticated user" \ |
| 257 | + "$ACPCTL" whoami |
| 258 | + |
| 259 | +# ── 2: create credentials ─────────────────────────────────────────────────── |
| 260 | + |
| 261 | +announce "2 · Create credentials" |
| 262 | + |
| 263 | +step "Create GitHub credential: ${CRED_GITHUB}" \ |
| 264 | + "$ACPCTL" credential create \ |
| 265 | + --name "${CRED_GITHUB}" \ |
| 266 | + --provider github \ |
| 267 | + --token "${GITHUB_TOKEN_VALUE}" \ |
| 268 | + --description "GitHub PAT for credential demo" |
| 269 | + |
| 270 | +step "Create GitLab credential: ${CRED_GITLAB}" \ |
| 271 | + "$ACPCTL" credential create \ |
| 272 | + --name "${CRED_GITLAB}" \ |
| 273 | + --provider gitlab \ |
| 274 | + --token "${GITLAB_TOKEN_VALUE}" \ |
| 275 | + --url "${GITLAB_URL}" \ |
| 276 | + --description "GitLab PAT for credential demo" |
| 277 | + |
| 278 | +step "Create Jira credential: ${CRED_JIRA}" \ |
| 279 | + "$ACPCTL" credential create \ |
| 280 | + --name "${CRED_JIRA}" \ |
| 281 | + --provider jira \ |
| 282 | + --token "${JIRA_TOKEN_VALUE}" \ |
| 283 | + --url "${JIRA_URL}" \ |
| 284 | + --email "${JIRA_EMAIL}" \ |
| 285 | + --description "Jira API token for credential demo" |
| 286 | + |
| 287 | +step "List all credentials" \ |
| 288 | + "$ACPCTL" credential list |
| 289 | + |
| 290 | +# ── 3: create project ─────────────────────────────────────────────────────── |
| 291 | + |
| 292 | +announce "3 · Create project" |
| 293 | + |
| 294 | +step "Create project: ${PROJECT_NAME}" \ |
| 295 | + "$ACPCTL" create project \ |
| 296 | + --name "${PROJECT_NAME}" \ |
| 297 | + --description "Credential lifecycle demo" |
| 298 | + |
| 299 | +CREATED_PROJECT="${PROJECT_NAME}" |
| 300 | + |
| 301 | +step "Set project context" \ |
| 302 | + "$ACPCTL" project "${PROJECT_NAME}" |
| 303 | + |
| 304 | +# ── 4: bind credentials to project ────────────────────────────────────────── |
| 305 | + |
| 306 | +announce "4 · Bind credentials to project" |
| 307 | + |
| 308 | +step "Bind GitHub credential" \ |
| 309 | + "$ACPCTL" credential bind "${CRED_GITHUB}" --project "${PROJECT_NAME}" |
| 310 | + |
| 311 | +step "Bind GitLab credential" \ |
| 312 | + "$ACPCTL" credential bind "${CRED_GITLAB}" --project "${PROJECT_NAME}" |
| 313 | + |
| 314 | +step "Bind Jira credential" \ |
| 315 | + "$ACPCTL" credential bind "${CRED_JIRA}" --project "${PROJECT_NAME}" |
| 316 | + |
| 317 | +# ── 5: create agent + start session ───────────────────────────────────────── |
| 318 | + |
| 319 | +announce "5 · Create agent and start session" |
| 320 | + |
| 321 | +sep; bold "▶ Create agent: ${AGENT_NAME}"; sleep "$PAUSE" |
| 322 | +AGENT_JSON=$( |
| 323 | + "$ACPCTL" agent create \ |
| 324 | + --project-id "${PROJECT_NAME}" \ |
| 325 | + --name "${AGENT_NAME}" \ |
| 326 | + --prompt "You are a credential verification agent. When asked, you confirm which credentials are available to you by listing their provider, name, and whether the token is present." \ |
| 327 | + -o json 2>/dev/null |
| 328 | +) |
| 329 | +AGENT_ID=$(json_field "$AGENT_JSON" "id") |
| 330 | +[[ -n "${AGENT_ID}" ]] || die "Failed to parse agent ID" |
| 331 | +green " agent created: ${AGENT_ID}" |
| 332 | +echo |
| 333 | + |
| 334 | +sep; bold "▶ Start session via agent"; sleep "$PAUSE" |
| 335 | +printf '\033[38;5;214m $ %s\033[0m\n' "acpctl start ${AGENT_ID} --project-id ${PROJECT_NAME}" |
| 336 | +START_OUTPUT=$( |
| 337 | + "$ACPCTL" start "${AGENT_ID}" \ |
| 338 | + --project-id "${PROJECT_NAME}" \ |
| 339 | + --prompt "List all credentials available to you. For each one, report: provider, name, and whether the token is non-empty. Do NOT print the actual token value." \ |
| 340 | + 2>&1 |
| 341 | +) |
| 342 | +echo " ${START_OUTPUT}" |
| 343 | + |
| 344 | +SESSION_ID=$( |
| 345 | + echo "${START_OUTPUT}" | sed -n 's|^session/\([^ ]*\) started.*|\1|p' |
| 346 | +) |
| 347 | +if [[ -z "${SESSION_ID}" ]]; then |
| 348 | + red " Failed to parse session ID from start output" |
| 349 | + die "Expected output like: session/<id> started (phase: ...)" |
| 350 | +fi |
| 351 | +CREATED_SESSION_ID="${SESSION_ID}" |
| 352 | +green " session: ${SESSION_ID}" |
| 353 | +echo |
| 354 | + |
| 355 | +# ── wait for Running ───────────────────────────────────────────────────────── |
| 356 | + |
| 357 | +announce "5b · Wait for session Running" |
| 358 | + |
| 359 | +wait_for_running "${SESSION_ID}" || die "Session did not reach Running phase" |
| 360 | + |
| 361 | +# ── 6: verify credentials in session ──────────────────────────────────────── |
| 362 | + |
| 363 | +announce "6 · Verify credentials in session" |
| 364 | + |
| 365 | +sep |
| 366 | +bold "▶ Send verification message and stream response" |
| 367 | +printf '\033[38;5;214m $ %s\033[0m\n' "acpctl session send ${SESSION_ID} \"...\" -f" |
| 368 | +sleep "$PAUSE" |
| 369 | + |
| 370 | +"$ACPCTL" session send "${SESSION_ID}" \ |
| 371 | + "List every credential available to this session. For each credential, report: 1) provider name, 2) credential name, 3) whether a token value is present (yes/no). Do NOT reveal the actual token. Format as a simple table." \ |
| 372 | + -f || yellow " stream ended (may have timed out)" |
| 373 | + |
| 374 | +echo |
| 375 | +step "Session messages" \ |
| 376 | + "$ACPCTL" session messages "${SESSION_ID}" |
| 377 | + |
| 378 | +# ── done ───────────────────────────────────────────────────────────────────── |
| 379 | + |
| 380 | +echo |
| 381 | +sep |
| 382 | +green " Demo complete" |
| 383 | +dim " Project ${PROJECT_NAME} and credentials will be deleted by cleanup." |
| 384 | +sep |
| 385 | +echo |
0 commit comments