Skip to content

Commit 878e284

Browse files
Panopticon Agentclaude
andcommitted
feat(setup-repo): repo-aware summaries + gh-login GH_TOKEN, DRY token storage
Build on #308's GH_TOKEN support with the operator-UX improvements: * Open with two bulleted lists — what we know about the repo (name + local-checkout vs GitHub-remote, classified from the git URL) and what its setup entails (Claude credential always; GH_TOKEN for GitHub repos), each marked needed / already-configured / not-needed. * GH_TOKEN acquisition now combines both paths: reuse a GH_TOKEN already in the environment (#308's fast path) when present, else authenticate interactively via `gh auth login` + `gh auth token`. Offers to replace an existing GH_TOKEN, and guides the operator when `gh` isn't installed. * DRY the env-file write: generalize `store_oauth_token` into `store_env_token <VAR> <token> <file>`, the single comment-out/replace/append implementation both tokens share (retires the append-only `append_env_var`); `store_oauth_token` stays as a thin wrapper. * Turn the closing summary into a bullet-per-step list. * Fold in #309's hint wording (detach reassurance + actionable drop). The opening summary needs the repo's name, so the ShellRunner now also exports PANOPTICON_REPO_NAME (alongside #308's PANOPTICON_GIT_URL), passed through by the spawner from the repo record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bef34b9 commit 878e284

7 files changed

Lines changed: 311 additions & 128 deletions

File tree

src/panopticon/sessionservice/shell_runner.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def spawn(
6868
*,
6969
env_file: str | None = None,
7070
git_url: str | None = None,
71+
repo_name: str | None = None,
7172
script: str = "",
7273
workdir: str | None = None,
7374
progress: Callable[[LifecyclePhase], None] | None = None,
@@ -82,7 +83,8 @@ def spawn(
8283
exported — so the script can drive its own lifecycle over REST (e.g. advance to COMPLETE on
8384
success) — and the repo's ``env_file`` secrets sourced first when given. ``git_url``, when
8485
given, is exported as ``PANOPTICON_GIT_URL`` so a script can tell what forge the repo lives
85-
on (e.g. offer to record a GitHub token). ``env_file`` is a
86+
on (e.g. offer to record a GitHub token); ``repo_name`` is exported as ``PANOPTICON_REPO_NAME``
87+
so a script can name the repo in its summary. ``env_file`` is a
8688
**name relative to this runner's secrets dir** (ADR 0007), resolved host-locally (like
8789
``LocalRunner``) so a remote runner uses its own host's secrets. The panopticon shell lib
8890
(``panopticon_advance``/``_drop``/…) is loaded into the shell so the script can drive its task
@@ -113,6 +115,7 @@ def _report(phase: LifecyclePhase) -> None:
113115
f"export PANOPTICON_TASK_ID={shlex.quote(task_id)}",
114116
f"export PANOPTICON_RUNNER_ID={shlex.quote(self._runner_id)}",
115117
*([f"export PANOPTICON_GIT_URL={shlex.quote(git_url)}"] if git_url else []),
118+
*([f"export PANOPTICON_REPO_NAME={shlex.quote(repo_name)}"] if repo_name else []),
116119
f"curl --silent --no-buffer {shlex.quote(live_url)} >/dev/null 2>&1 &",
117120
"_panopticon_live_pid=$!",
118121
"trap 'kill $_panopticon_live_pid 2>/dev/null' EXIT",

src/panopticon/sessionservice/spawner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def _spawn_shell(self, task: JsonObj, repo: JsonObj) -> str:
259259
task_id,
260260
env_file=repo.get("env_file"), # per-repo secrets, sourced into the shell (ADR 0007)
261261
git_url=repo.get("git_url"), # the repo's forge — lets a script detect a GitHub remote
262+
repo_name=repo.get("name"), # so a script can name the repo in its summary
262263
script=spec["script"],
263264
workdir=workdir,
264265
progress=lambda phase: self._report(task_id, phase), # STARTING then AWAITING

src/panopticon/workflows/setup_repo.sh

Lines changed: 157 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,81 @@
1-
# Collect a Claude auth token (`claude setup-token`) for the repo's env-file. Run by the session
2-
# service in a host tmux session (no container); ShellRunner sources the repo's env-file first, so an
3-
# already-configured credential shows up as an env var, and exports PANOPTICON_ENV_FILE (its path)
4-
# and PANOPTICON_GIT_URL (the repo's remote, used to detect a GitHub forge below).
1+
# Guide the operator through a repo's host-side setup: mint a Claude auth token (`claude
2+
# setup-token`) and — for a GitHub repo — record a `GH_TOKEN`, writing each into the repo's env-file.
3+
# Run by the session service in a host tmux session (no container); ShellRunner sources the repo's
4+
# env-file first (so an already-configured credential shows up as an env var) and exports
5+
# PANOPTICON_ENV_FILE (its path), PANOPTICON_GIT_URL (the repo's remote, used to detect a GitHub
6+
# forge below), and PANOPTICON_REPO_NAME (the repo's label, for the summary).
57
#
6-
# Whatever route the operator takes, the script converges on a summary + a prompt to press Enter,
7-
# which completes the task and returns them to the dashboard.
8+
# Whatever route the operator takes, the script converges on a bulleted summary + a prompt to press
9+
# Enter, which completes the task and returns them to the dashboard.
810

911
env_file="${PANOPTICON_ENV_FILE:-the repo's env-file}"
12+
repo_name="${PANOPTICON_REPO_NAME:-this repo}"
13+
repo_url="${PANOPTICON_GIT_URL:-}"
14+
repo_label=$(repo_source_label "$repo_url")
1015
1116
# How to get back to the dashboard: detach from this tmux session. Detect the prefix + detach key
1217
# from the running server (the operator may have rebound them), falling back to the tmux defaults.
1318
prefix=$(tmux show-options -gv prefix 2>/dev/null)
1419
[ -n "$prefix" ] || prefix="C-b"
1520
detach=$(tmux list-keys -T prefix 2>/dev/null | awk '$NF == "detach-client" { print $(NF - 1); exit }')
1621
[ -n "$detach" ] || detach="d"
17-
dashboard_hint="To return to the dashboard without finishing, detach: press $prefix then $detach (the task stays running)."
22+
dashboard_hint="To return to the dashboard without finishing, detach: press $prefix then $detach (you can resume this task any time from the dashboard)."
1823
1924
# Show how to get back to the dashboard up front, before anything else.
2025
echo "$dashboard_hint"
2126
echo
2227
23-
summary=""
28+
# Work out what's already configured and what setting this repo up entails. The Claude credential is
29+
# always needed (the agent runs `claude` regardless); a GH_TOKEN is only needed for a GitHub remote
30+
# (a local checkout has nothing to push). "Configured" means the env-file already carries it.
31+
claude_configured=0
32+
if [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] || [ -n "${ANTHROPIC_API_KEY:-}" ]; then
33+
claude_configured=1
34+
fi
35+
gh_needed=0
36+
gh_configured=0
37+
if is_github_url "$repo_url"; then
38+
gh_needed=1
39+
env_file_has_var GH_TOKEN "${PANOPTICON_ENV_FILE:-}" && gh_configured=1
40+
fi
2441
25-
# Append clause $1 to the running $summary, space-separating it from anything already there. Each
26-
# step records its own outcome this way, so the order the steps run in doesn't clobber the summary.
42+
# What we know about the repo, and what its setup entails — two bulleted lists up front.
43+
echo "This repo:"
44+
echo " • Name: $repo_name"
45+
echo " • Source: $repo_label"
46+
echo
47+
echo "To set up:"
48+
if [ "$claude_configured" -eq 1 ]; then
49+
echo " • Claude credential — already configured"
50+
else
51+
echo " • Claude credential — needed"
52+
fi
53+
if [ "$gh_needed" -eq 1 ]; then
54+
if [ "$gh_configured" -eq 1 ]; then
55+
echo " • GH_TOKEN — already configured"
56+
else
57+
echo " • GH_TOKEN — needed (GitHub repo)"
58+
fi
59+
else
60+
echo " • GH_TOKEN — not needed (not a GitHub repo)"
61+
fi
62+
echo
63+
64+
# The closing summary is a bullet per step; each step appends its outcome here.
65+
summary=""
2766
add_summary() {
28-
if [ -n "$summary" ]; then
29-
summary="$summary $1"
67+
if [ -z "$summary" ]; then
68+
summary=" $1"
3069
else
31-
summary="$1"
70+
summary="$summary
71+
$1"
3272
fi
3373
}
3474
35-
# Mint a token and record the outcome in $summary. On success, capture the minted token and write it
36-
# straight into the repo's env-file (commenting out any previous one — see store_oauth_token); fall
75+
# Mint a Claude token and record the outcome. On success, capture the minted token and write it
76+
# straight into the repo's env-file (commenting out any previous one — see store_env_token); fall
3777
# back to on-screen copy instructions when it can't be captured or there's no env-file to write to.
38-
# extract_oauth_token / store_oauth_token come from setup_repo_lib.sh (prepended by shell_script()).
78+
# extract_oauth_token / store_env_token come from setup_repo_lib.sh (prepended by shell_script()).
3979
collect_token() {
4080
echo
4181
echo "Running 'claude setup-token' — follow the prompts to mint a token."
@@ -60,66 +100,108 @@ collect_token() {
60100
fi
61101
62102
if [ "$_ct_ok" -eq 0 ]; then
63-
add_summary "'claude setup-token' failed or was cancelled — no token was collected."
103+
add_summary "Claude credential: 'claude setup-token' failed or was cancelled — nothing collected."
64104
elif [ -n "$_ct_token" ] && [ -n "${PANOPTICON_ENV_FILE:-}" ] \
65-
&& store_oauth_token "$_ct_token" "$PANOPTICON_ENV_FILE"; then
105+
&& store_env_token CLAUDE_CODE_OAUTH_TOKEN "$_ct_token" "$PANOPTICON_ENV_FILE"; then
66106
echo
67107
echo "Wrote the new token to $env_file as CLAUDE_CODE_OAUTH_TOKEN (any previous one was commented out)."
68-
add_summary "Minted a new token and wrote it to $env_file (any previous token was commented out)."
108+
add_summary "Claude credential: minted a new token and wrote it to $env_file (any previous one was commented out)."
69109
else
70110
# Minted, but we couldn't capture/extract it or there's no env-file configured — guide the copy.
71111
echo
72112
echo "Token minted. Copy the token shown above into $env_file as:"
73113
echo " CLAUDE_CODE_OAUTH_TOKEN=<token>"
74-
add_summary "Minted a new token — copy it into $env_file as CLAUDE_CODE_OAUTH_TOKEN."
114+
add_summary "Claude credential: minted a new token — copy it into $env_file as CLAUDE_CODE_OAUTH_TOKEN."
115+
fi
116+
}
117+
118+
# Write a GH token into the env-file, or record why we couldn't — shared by both the reuse-from-env
119+
# and `gh auth login` paths (mirrors the Claude token's store step). $1 ok flag, $2 token, $3 the
120+
# source label for the summary. Goes through store_env_token, so an existing GH_TOKEN is commented
121+
# out and replaced just like the Claude token.
122+
store_gh_token() {
123+
if [ "$1" -eq 0 ]; then
124+
add_summary "GH_TOKEN: $3 failed or was cancelled — nothing collected."
125+
elif [ -n "$2" ] && [ -n "${PANOPTICON_ENV_FILE:-}" ] \
126+
&& store_env_token GH_TOKEN "$2" "$PANOPTICON_ENV_FILE"; then
127+
echo
128+
echo "Wrote GH_TOKEN to $env_file (any previous one was commented out)."
129+
add_summary "GH_TOKEN: wrote it to $env_file from $3 (any previous one was commented out)."
130+
else
131+
echo
132+
echo "Couldn't write GH_TOKEN. Add it to $env_file yourself:"
133+
echo " GH_TOKEN=<a GitHub token>"
134+
add_summary "GH_TOKEN: couldn't write it — add GH_TOKEN to $env_file yourself."
75135
fi
76136
}
77137
78-
# Offer to record a GitHub token in the repo's env-file, but only when it's both wanted and missing:
79-
# the repo is hosted on GitHub (PANOPTICON_GIT_URL), a GH_TOKEN is present in the environment (e.g.
80-
# the operator's own shell — the shell runner inherits the host env), and the env-file doesn't
81-
# already carry an active GH_TOKEN line. Records the outcome in $summary. is_github_url /
82-
# env_file_has_var / append_env_var come from setup_repo_lib.sh (prepended by shell_script()).
83-
maybe_offer_github_token() {
84-
is_github_url "${PANOPTICON_GIT_URL:-}" || return 0
85-
[ -n "${GH_TOKEN:-}" ] || return 0
86-
[ -n "${PANOPTICON_ENV_FILE:-}" ] || return 0
87-
! env_file_has_var GH_TOKEN "$PANOPTICON_ENV_FILE" || return 0
138+
# Authenticate to GitHub with `gh auth login`, then capture the token with `gh auth token` and store
139+
# it — the fallback when there's no GH_TOKEN in the environment to reuse. Guarded on `gh` being
140+
# installed; otherwise guides the operator to add one by hand.
141+
collect_gh_token() {
142+
if ! command -v gh >/dev/null 2>&1; then
143+
echo
144+
echo "The 'gh' CLI isn't installed on this host, so I can't run 'gh auth login'."
145+
echo "Add a token to $env_file yourself instead:"
146+
echo " GH_TOKEN=<a GitHub token, e.g. from 'gh auth token'>"
147+
add_summary "GH_TOKEN: 'gh' not installed — add GH_TOKEN to $env_file yourself."
148+
return
149+
fi
88150
echo
89-
echo "This repo is hosted on GitHub and a GH_TOKEN is set in your environment, but $env_file"
90-
echo "has no GH_TOKEN. Adding it lets task containers use 'gh' and push over HTTPS."
151+
echo "Running 'gh auth login' — follow the prompts to authenticate to GitHub."
91152
echo
92-
printf 'Add GH_TOKEN to %s? [y/N] ' "$env_file"
93-
read gh_answer
94-
case "$gh_answer" in
95-
[Yy]*)
96-
if append_env_var GH_TOKEN "$GH_TOKEN" "$PANOPTICON_ENV_FILE"; then
97-
echo "Wrote GH_TOKEN to $env_file."
98-
add_summary "Added GH_TOKEN to $env_file."
99-
else
100-
echo "Could not write GH_TOKEN to $env_file."
101-
add_summary "Could not add GH_TOKEN to $env_file."
102-
fi
103-
;;
104-
*) add_summary "Left GH_TOKEN out of $env_file." ;;
105-
esac
153+
_gt_ok=1
154+
gh auth login || _gt_ok=0
155+
_gt_token=""
156+
[ "$_gt_ok" -eq 1 ] && _gt_token=$(gh auth token 2>/dev/null)
157+
store_gh_token "$_gt_ok" "$_gt_token" "'gh auth login'"
106158
}
107159
108-
if [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] || [ -n "${ANTHROPIC_API_KEY:-}" ]; then
160+
# Get a GH_TOKEN into the env-file for a GitHub repo: reuse one already in the environment if the
161+
# operator has it (the shell runner inherits the host env — the fast path), else authenticate with
162+
# `gh auth login`.
163+
setup_gh_token() {
164+
if [ -n "${GH_TOKEN:-}" ]; then
165+
echo "A GH_TOKEN is set in your environment. Adding it to $env_file lets task containers use"
166+
echo "'gh' and push over HTTPS."
167+
echo
168+
printf 'Add the GH_TOKEN from your environment to %s? [Y/n] ' "$env_file"
169+
read gh_answer
170+
case "$gh_answer" in
171+
[Nn]*) add_summary "GH_TOKEN: skipped — add GH_TOKEN to $env_file yourself." ;;
172+
*) store_gh_token 1 "$GH_TOKEN" "your environment" ;;
173+
esac
174+
else
175+
echo "No GH_TOKEN in your environment — a GitHub repo needs one to push and open PRs."
176+
echo
177+
echo "Prefer to use your own? Press $prefix then $detach to go to the dashboard, drop this task using 'x' and add"
178+
echo " GH_TOKEN=<a GitHub token>"
179+
echo "to $env_file yourself."
180+
echo
181+
printf 'Authenticate to GitHub with gh now? [Y/n] '
182+
read gh_answer
183+
case "$gh_answer" in
184+
[Nn]*) add_summary "GH_TOKEN: skipped — add GH_TOKEN to $env_file yourself." ;;
185+
*) collect_gh_token ;;
186+
esac
187+
fi
188+
}
189+
190+
# --- Claude credential -------------------------------------------------------------------------
191+
if [ "$claude_configured" -eq 1 ]; then
109192
echo "A Claude credential is already configured in $env_file."
110-
echo "To keep using it, drop this task instead (press 'x' in the dashboard)."
111193
echo
112194
printf 'Collect a new token anyway? [y/N] '
113195
read answer
114196
case "$answer" in
115197
[Yy]*) collect_token ;;
116-
*) add_summary "Kept the existing credential in $env_file — nothing collected." ;;
198+
*) add_summary "Claude credential: kept the existing one in $env_file — nothing collected." ;;
117199
esac
118200
else
119201
echo "No Claude credential found in $env_file."
120202
echo "About to collect one with 'claude setup-token'."
121203
echo
122-
echo "Prefer to use your own? Drop this task (press 'x' in the dashboard) and add one of"
204+
echo "Prefer to use your own? Press $prefix then $detach to go to the dashboard, drop this task using 'x' and add one of"
123205
echo "these to $env_file yourself:"
124206
echo " CLAUDE_CODE_OAUTH_TOKEN=<token from 'claude setup-token'>"
125207
echo " ANTHROPIC_API_KEY=<your Anthropic API key>"
@@ -129,14 +211,33 @@ else
129211
collect_token
130212
fi
131213
132-
# With the Claude credential settled, offer to record a GitHub token too (no-op unless it applies).
133-
maybe_offer_github_token
214+
# --- GH_TOKEN (GitHub repos only) --------------------------------------------------------------
215+
if [ "$gh_needed" -eq 1 ]; then
216+
echo
217+
if [ "$gh_configured" -eq 1 ]; then
218+
echo "A GH_TOKEN is already configured in $env_file."
219+
echo
220+
printf 'Replace it? [y/N] '
221+
read answer
222+
case "$answer" in
223+
[Yy]*) setup_gh_token ;;
224+
*) add_summary "GH_TOKEN: kept the existing one in $env_file — nothing collected." ;;
225+
esac
226+
else
227+
setup_gh_token
228+
fi
229+
fi
134230
135-
# Every route converges here: summarize what happened, then complete the task on Enter (which ends
136-
# the session and returns the operator to the dashboard; detaching instead — see the hint above —
137-
# leaves it running).
231+
# Every route converges here: summarize what happened (a bullet per step), then complete the task on
232+
# Enter (which ends the session and returns the operator to the dashboard; detaching instead — see
233+
# the hint above — leaves it running).
138234
echo
139-
echo "Summary: $summary"
235+
echo "Summary:"
236+
if [ -n "$summary" ]; then
237+
echo "$summary"
238+
else
239+
echo " • Nothing to do — everything was already configured."
240+
fi
140241
echo
141242
printf 'Press Enter to complete this task and return to the dashboard. '
142243
read _

0 commit comments

Comments
 (0)