Skip to content

Commit 23d8958

Browse files
userclaude
andcommitted
fix(runner,cp): credential token fetch uses correct path + project-scoped binding
Runner: fix 404 by using global credential token path (/api/ambient/v1/credentials/{id}/token) matching the API server route, instead of the non-existent project-scoped path. Control plane: resolveCredentialIDs now queries role bindings filtered by scope=credential and project_id, so runners only receive credentials explicitly bound to their project — not every credential in the system. Also adds demo-credentials.sh replacing the stale demo-github.sh with a spec-compliant credential lifecycle demo (create, bind, verify). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a5ef660 commit 23d8958

3 files changed

Lines changed: 404 additions & 10 deletions

File tree

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
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

Comments
 (0)