Phase 5 of docs/agents/GITEA-SETUP.md, plus a correction to Phase 2 that the runbook could not have known it needed. gitea-mcp v1.7.0 goes into both images, pinned by the sha256 the release publishes and smoke-tested with `--version` at build time, so a bad pin fails the build instead of the agent. Each entrypoint registers it at user scope for that container's own identity, remove-then-add so a restart is idempotent. The token is passed BY PATH. `-e GITEA_ACCESS_TOKEN=$(cat …)` would write it in cleartext into ~/.claude.json, which every session in the container reads; GITEA_ACCESS_TOKEN_FILE is new in the pinned version and leaves the secret in its read-only mount. Verified against the binary's own --help, not assumed. The tool filter stops being an experiment. The names are in the release README: each agent gets issues, notifications, labels, milestones and pull requests, and NOT `pull_request_review_write`. That one matters because separate identities open a hole the runbook did not name: Gitea refuses to let an author approve their own pull request, and does nothing about sylph-decoder approving sylph-port's. Two agents could satisfy `required_approvals = 1` between themselves and then merge, since branch protection blocks pushes to main and never blocked merges. Withholding the tool is defence in depth; the controls are in branch protection, and both docs now say so: approvals whitelisted to the human so an agent's approval does not count, merges whitelisted to the human so an approved PR is still merged by a person. Phase 2's check gains the step that actually tests it -- approve the throwaway PR yourself, then confirm the agent STILL has no merge button. Without that step, the check passes on an instance where the agents can merge each other's work. Also settles two entries on the runbook's own "not verified" list: the tool filter names, and the Gitea version (1.25.5, whose API schema carries enable_merge_whitelist and enable_approvals_whitelist under those names). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
360 lines
20 KiB
Bash
Executable File
360 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Bring up the headless display, then hand over to the command.
|
|
#
|
|
# Xvfb and openbox are started HERE, as children of PID 1 (tini), rather than by
|
|
# the toolkit scripts. That is the fix for the long-standing "Xvfb and the
|
|
# emulator die on their own every few minutes" note: nothing owned those
|
|
# processes, so nothing kept them alive, and a run could sit for 300 s in front
|
|
# of a visible MAIN MENU reporting "no main menu" because the display had gone.
|
|
# A container-lifetime display makes that failure mode impossible.
|
|
set -euo pipefail
|
|
|
|
log() { printf '[entrypoint] %s\n' "$*" >&2; }
|
|
|
|
DISPLAY="${DISPLAY:-:98}"
|
|
GEOM="${SCREEN_GEOMETRY:-1280x720x24}"
|
|
export DISPLAY
|
|
|
|
# ── Display ──────────────────────────────────────────────────────────────────
|
|
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
|
|
rm -f "/tmp/.X${DISPLAY#:}-lock" "/tmp/.X11-unix/X${DISPLAY#:}" 2>/dev/null || true
|
|
# GLX and RANDR are both required: Canary's window is GTK+OpenGL even when the
|
|
# graphics backend is Vulkan, and xwininfo-based screen oracles need RANDR.
|
|
Xvfb "$DISPLAY" -screen 0 "$GEOM" -ac -nolisten tcp \
|
|
+extension GLX +extension RANDR >/tmp/xvfb.log 2>&1 &
|
|
for _ in $(seq 1 50); do
|
|
xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break
|
|
sleep 0.2
|
|
done
|
|
fi
|
|
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
|
|
log "FATAL: no display on $DISPLAY — see /tmp/xvfb.log"
|
|
exit 1
|
|
fi
|
|
|
|
# A window manager is not cosmetic here: without one the emulator window is
|
|
# never mapped at a known position, and every pixel oracle reads the wrong rows.
|
|
if ! pgrep -x openbox >/dev/null 2>&1; then
|
|
openbox >/tmp/openbox.log 2>&1 &
|
|
sleep 0.5
|
|
fi
|
|
log "display $DISPLAY ready ($GEOM)"
|
|
|
|
# ── Vulkan ───────────────────────────────────────────────────────────────────
|
|
# Prefer the real GPU when /dev/dri was passed through; fall back to lavapipe,
|
|
# which is slow but correct and needs no host device.
|
|
LVP=$(ls /usr/share/vulkan/icd.d/lvp_icd*.json 2>/dev/null | head -1)
|
|
if [ "${SYLPH_VULKAN:-auto}" = "sw" ] || [ ! -e /dev/dri/renderD128 ]; then
|
|
[ -n "$LVP" ] && export VK_ICD_FILENAMES="$LVP"
|
|
else
|
|
unset LIBGL_ALWAYS_SOFTWARE
|
|
fi
|
|
# Report what Vulkan ACTUALLY enumerated, not what we asked for. Announcing
|
|
# "host GPU via /dev/dri" because the device node exists is how an agent ends up
|
|
# believing it has hardware while running llvmpipe — an NVIDIA card needs the
|
|
# NVIDIA Container Toolkit, and /dev/dri alone does nothing for it.
|
|
if command -v vulkaninfo >/dev/null 2>&1; then
|
|
vkdev=$(vulkaninfo --summary 2>/dev/null | awk -F= '/deviceName/{gsub(/^ +| +$/,"",$2); print $2; exit}')
|
|
case "${vkdev:-}" in
|
|
"") log "Vulkan: NO DEVICE — vulkaninfo enumerated nothing" ;;
|
|
llvmpipe*|lavapipe*) log "Vulkan: $vkdev (SOFTWARE — correct but slow)" ;;
|
|
*) log "Vulkan: $vkdev (hardware)" ;;
|
|
esac
|
|
fi
|
|
|
|
# ── Build parallelism ────────────────────────────────────────────────────────
|
|
# Bounded by MEMORY, not just cores. A full-parallel build of this tree has
|
|
# OOM-killed the host outright, and inside a half-the-box container the ceiling
|
|
# is lower still. ~1.5 GiB per C++ TU is the rule of thumb that has held.
|
|
# nproc reports the HOST's core count: --cpus is a CFS quota, not a mask. Using
|
|
# it would oversubscribe a half-the-box container by exactly 2x, so read the
|
|
# quota the cgroup actually grants.
|
|
cpus=$(nproc)
|
|
if [ -r /sys/fs/cgroup/cpu.max ]; then
|
|
read -r _q _p < /sys/fs/cgroup/cpu.max || true
|
|
if [ "${_q:-max}" != max ] && [ "${_p:-0}" -gt 0 ]; then
|
|
cpus=$(( (_q + _p - 1) / _p ))
|
|
[ "$cpus" -lt 1 ] && cpus=1
|
|
fi
|
|
fi
|
|
mem_gib=$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)
|
|
# MemAvailable is the HOST's too under cgroup v2; prefer the container's cap.
|
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
|
_m=$(cat /sys/fs/cgroup/memory.max)
|
|
[ "$_m" != max ] && mem_gib=$(( _m / 1073741824 ))
|
|
fi
|
|
[ "${mem_gib:-0}" -lt 1 ] && mem_gib=1
|
|
# 🔴 THIS CONSTANT WAS WRONG, AND IT COST A RUN. `mem_gib * 2 / 3` assumes
|
|
# ~1.5 GB per job. On 2026-09-01 a raw `cargo test --release -p sylpheed-formats`
|
|
# ran 4 jobs in a 6 GB container and was OOM-killed mid-task. Release-mode rustc
|
|
# on this workspace needs closer to 2 GB, so the divisor is 2, not 3/2.
|
|
#
|
|
# ⚠️ And the kill reported `OOMKilled: true` with **ExitCode 0**, so it read as a
|
|
# clean exit — which is why the restart policy is `unless-stopped` rather than
|
|
# `on-failure`.
|
|
by_mem=$(( mem_gib / 2 ))
|
|
[ "$by_mem" -lt 1 ] && by_mem=1
|
|
jobs=$(( cpus < by_mem ? cpus : by_mem ))
|
|
# An EXPLICIT cap from the launcher wins. Without this the launcher's
|
|
# `-e CARGO_BUILD_JOBS=...` was computed, exported over, and silently discarded
|
|
# — the guardrail was set and then removed three lines later.
|
|
if [ -n "${CARGO_BUILD_JOBS:-}" ] && [ "${CARGO_BUILD_JOBS}" -ge 1 ] 2>/dev/null; then
|
|
jobs="$CARGO_BUILD_JOBS"
|
|
_why=" (explicit, from the launcher)"
|
|
fi
|
|
export SYLPH_JOBS="$jobs" CARGO_BUILD_JOBS="$jobs" CMAKE_BUILD_PARALLEL_LEVEL="$jobs"
|
|
log "build parallelism: $jobs${_why:-} (cpus=$cpus, mem=${mem_gib}GiB avail)"
|
|
|
|
mkdir -p "$HOME/shots" "$HOME/logs"
|
|
|
|
# ── The repository, cloned into THIS AGENT'S OWN volume ─────────────────────
|
|
# Not a bind mount of a human's working tree. That arrangement bit this project
|
|
# three times: an agent's `git config --local` captured a human's commits, a
|
|
# credential helper leaked a container-only path onto the host, and a `git add
|
|
# -A` swept an agent's in-flight files into somebody else's commit. Separate
|
|
# checkouts make all three impossible rather than discouraged.
|
|
#
|
|
# Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out
|
|
# from under whatever it is mid-edit, which is the same class of bug again.
|
|
if ! git -C /work rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
# Checks for a usable HEAD, not merely a .git directory. A clone interrupted
|
|
# partway -- the container stopped while it ran, which has happened -- leaves
|
|
# a .git with no commits, and a presence check would then skip the retry
|
|
# forever and hand the agent an empty repository.
|
|
echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work"
|
|
_tmp=$(mktemp -d)
|
|
if git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" "$_tmp/r"; then
|
|
find /work -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
mv "$_tmp/r"/.[!.]* "$_tmp/r"/* /work/ 2>/dev/null || true
|
|
echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
|
|
else
|
|
echo "[entrypoint] clone FAILED -- the agent has no repository" >&2
|
|
fi
|
|
rm -rf "$_tmp"
|
|
else
|
|
echo "[entrypoint] /work at $(git -C /work rev-parse --short HEAD) on $(git -C /work rev-parse --abbrev-ref HEAD)"
|
|
fi
|
|
|
|
# The shared exchange, for transient files that must not enter git history.
|
|
mkdir -p /exchange/files 2>/dev/null || true
|
|
|
|
# ── Claude Code config ───────────────────────────────────────────────────────
|
|
# ── Claude state: this agent's own, seeded once from the host ───────────────
|
|
# Isolated per agent. Both working directories are /work, and Claude Code keys
|
|
# its per-project state off the working directory -- so a SHARED ~/.claude put
|
|
# two independent agents in the same projects/-work/ directory, which undoes the
|
|
# point of giving them separate checkouts.
|
|
#
|
|
# Seeded rather than shared because credentials live in .credentials.json and a
|
|
# token refresh must be able to write. Copying once means each agent refreshes
|
|
# its own token and neither can corrupt the host's.
|
|
# Re-seed whenever the HOST's credentials are newer than ours, not only when
|
|
# ours are missing. The missing-only guard meant an expired token could never be
|
|
# replaced: the file existed, so the copy was skipped, and restarting the
|
|
# container changed nothing. A human re-logging in on the host is exactly the
|
|
# recovery path, and it has to reach here.
|
|
#
|
|
# Newer-wins rather than always-copy, because the container refreshes its own
|
|
# token during a run and that copy may legitimately be the fresher one.
|
|
# 🔴 A LONG-LIVED TOKEN WINS, AND THE SEEDING MUST NOT FIGHT IT.
|
|
#
|
|
# With CLAUDE_CODE_OAUTH_TOKEN set, copying the host's rotating credential file
|
|
# in would re-create the exact collision the token exists to remove: three
|
|
# clients on one rotating refresh token, the losers of a rotation race getting
|
|
# their stored tokens CLEARED to empty strings and parking at "Login expired".
|
|
# Measured 2026-09-04 -- see the launcher.
|
|
# 🔴 PER-AGENT LOGIN: never seed. Set SYLPH_OWN_LOGIN=1 once this container
|
|
# has run `claude auth login` itself. Its grant is its OWN -- copying the
|
|
# host's over it re-creates the rotation collision that empties credentials
|
|
# and parks the session, which is the whole reason per-agent logins exist.
|
|
if [ -n "${SYLPH_OWN_LOGIN:-}" ] && [ "${SYLPH_OWN_LOGIN}" != "0" ]; then
|
|
log "auth: this agent has its own login; not seeding from the host"
|
|
elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
|
|
log "auth: using the long-lived token from the environment; not seeding OAuth"
|
|
elif [ -d "$HOME/.claude.seed" ] && \
|
|
{ [ ! -s "$HOME/.claude/.credentials.json" ] || \
|
|
[ "$HOME/.claude.seed/.credentials.json" -nt "$HOME/.claude/.credentials.json" ]; }; then
|
|
mkdir -p "$HOME/.claude"
|
|
cp -a "$HOME/.claude.seed/.credentials.json" "$HOME/.claude/" 2>/dev/null || true
|
|
for f in settings.json CLAUDE.md; do
|
|
[ -e "$HOME/.claude.seed/$f" ] && cp -a "$HOME/.claude.seed/$f" "$HOME/.claude/" 2>/dev/null || true
|
|
done
|
|
echo "[entrypoint] refreshed ~/.claude credentials from the host"
|
|
fi
|
|
|
|
# Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as
|
|
# complete. Claude Code re-runs its first-run wizard whenever
|
|
# lastOnboardingVersion differs from the installed version — so a container with
|
|
# a newer Claude than the host stops on the theme picker, with no error and no
|
|
# log line, and an unattended agent sits there forever.
|
|
if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then
|
|
cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true
|
|
fi
|
|
|
|
# Nothing is restored into `~/.claude.json` on purpose. Resuming is done by
|
|
# SESSION ID off the transcript instead — see the resume block below for why
|
|
# the index is useless for this.
|
|
# `credential.helper=store` rewrites this file by rename-over-target, which
|
|
# fails with EBUSY on a bind mount -- reported as `fatal: unable to write
|
|
# credential store`, while the push itself succeeds. A fatal line that is
|
|
# routinely wrong teaches the reader to ignore the one that is real.
|
|
if [ -f "$HOME/.git-credentials.host" ]; then
|
|
cp "$HOME/.git-credentials.host" "$HOME/.git-credentials" 2>/dev/null || true
|
|
chmod 600 "$HOME/.git-credentials" 2>/dev/null || true
|
|
fi
|
|
|
|
CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE '^[0-9][0-9.]*' || echo 0.0.0)
|
|
python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" \
|
|
"$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true
|
|
chmod 600 "$HOME/.claude.json" 2>/dev/null || true
|
|
|
|
# ── The Gitea MCP server ─────────────────────────────────────────────────────
|
|
# Registered at USER scope rather than from a committed `.mcp.json`: the token
|
|
# differs per agent and none of it belongs in git.
|
|
#
|
|
# 🔴 THE TOKEN IS PASSED AS A PATH, NOT A VALUE. `-e GITEA_ACCESS_TOKEN=$(cat
|
|
# …)` would write the secret in cleartext into ~/.claude.json, where it is read
|
|
# by every session in this container and lands in any copy of that file.
|
|
# `GITEA_ACCESS_TOKEN_FILE` (gitea-mcp ≥ 1.7.0) leaves the token in its
|
|
# read-only mount and lets the server read it itself.
|
|
#
|
|
# Re-registered on every start, remove-then-add: `claude mcp add` refuses a name
|
|
# that already exists, and ~/.claude.json is re-seeded above — neither ordering
|
|
# survives alone.
|
|
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.sylph-gitea-token}"
|
|
GITEA_HOST_URL="${SYLPH_GITEA_HOST:-https://git.mc02.dev}"
|
|
# Which tools this agent gets. Deliberately not all of them:
|
|
#
|
|
# * `pull_request_review_write` IS ABSENT, and that is the load-bearing one.
|
|
# Gitea will not let an author approve its own pull request — but the moment
|
|
# the two agents are separate people, nothing stops them approving each
|
|
# OTHER's and satisfying `required_approvals` between themselves with no
|
|
# human involved. Separate identities open that hole; withholding the tool
|
|
# closes it here, and the approvals whitelist on `main` closes it there.
|
|
# * the file / branch / repo WRITE tools are absent: a change reaches `main`
|
|
# as a reviewable commit through git, or it does not reach it.
|
|
#
|
|
# `pull_request_write` bundles `merge` into one tool and cannot be split, so
|
|
# merging stays blocked where the agent cannot reach it — the merge whitelist in
|
|
# branch protection. This list is defence in depth BEHIND that, never instead.
|
|
GITEA_MCP_TOOLS="${SYLPH_GITEA_TOOLS:-get_me,notification_read,notification_write,list_issues,issue_read,issue_write,attachment_read,search_issues,label_read,milestone_read,list_pull_requests,pull_request_read,pull_request_write}"
|
|
if [ ! -s "$GITEA_TOKEN_FILE" ]; then
|
|
echo "[entrypoint] no Gitea token at $GITEA_TOKEN_FILE — MCP not registered."
|
|
echo "[entrypoint] This agent cannot read its notifications or open a pull"
|
|
echo "[entrypoint] request, which is most of what its brief asks of it."
|
|
elif ! command -v gitea-mcp >/dev/null 2>&1; then
|
|
echo "[entrypoint] gitea-mcp is not in this image — rebuild it." >&2
|
|
else
|
|
claude mcp remove gitea -s user >/dev/null 2>&1 || true
|
|
if claude mcp add -s user gitea \
|
|
-e "GITEA_ACCESS_TOKEN_FILE=$GITEA_TOKEN_FILE" \
|
|
-- gitea-mcp -t stdio -H "$GITEA_HOST_URL" -O "$GITEA_MCP_TOOLS" >/dev/null 2>&1; then
|
|
echo "[entrypoint] gitea MCP registered against $GITEA_HOST_URL"
|
|
else
|
|
echo "[entrypoint] gitea MCP registration FAILED — the agent has no issues," >&2
|
|
echo "[entrypoint] no pull requests and no notifications." >&2
|
|
fi
|
|
fi
|
|
|
|
# ── Claude Code ──────────────────────────────────────────────────────────────
|
|
if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then
|
|
# Drop the image's default CMD first, or `claude` is handed the literal string
|
|
# "bash" as its prompt and answers a question nobody asked.
|
|
if [ "$#" -eq 1 ] && [ "$1" = "bash" ]; then
|
|
set --
|
|
fi
|
|
# ── Resume across a restart ────────────────────────────────────────────────
|
|
#
|
|
# The container restarts automatically now, and a restart that opens a BLANK
|
|
# session throws away everything the agent knew. That is not hypothetical: on
|
|
# 2026-09-01 an OOM kill ended a run mid-task with a 2.7 MB transcript and two
|
|
# files uncommitted in the volume.
|
|
#
|
|
# 🔴 RESUME BY SESSION ID, NOT BY `--continue`. Measured 2026-09-01:
|
|
#
|
|
# `--continue` resolves through `~/.claude.json`'s per-project `history` and
|
|
# `lastSessionId`. Those are written at a GRACEFUL SHUTDOWN — mid-session the
|
|
# live file has `history: None`, `lastSessionId: None`. A container that is
|
|
# OOM-killed or `docker rm -f`ed never writes them, which is exactly the case
|
|
# this feature exists for. So `--continue` answered "No conversation found to
|
|
# continue" with 33 MB of perfectly good transcripts in the volume beside it,
|
|
# and persisting `.claude.json` did not help because the fields were never
|
|
# populated in the first place.
|
|
#
|
|
# The TRANSCRIPTS are durable and are named by session id, so read the id off
|
|
# the newest one for this working directory. Claude Code has not started yet
|
|
# at this point, so the newest is the previous run's.
|
|
#
|
|
# The /loop prompt is still passed, so the loop is RE-ARMED rather than merely
|
|
# restored — a resumed conversation with no wake-up scheduled answers once and
|
|
# stops, which looks like resuming and is not.
|
|
SYLPH_STAMP="$HOME/.claude/.sylph-last-start"
|
|
SYLPH_RESUME=0
|
|
SYLPH_SESSION=""
|
|
SYLPH_PROJ="$HOME/.claude/projects/$(printf '%s' "$PWD" | sed 's#/#-#g')"
|
|
if [ -d "$SYLPH_PROJ" ]; then
|
|
_newest=$(ls -1t "$SYLPH_PROJ"/*.jsonl 2>/dev/null | head -1)
|
|
if [ -n "$_newest" ]; then
|
|
SYLPH_SESSION=$(basename "$_newest" .jsonl)
|
|
SYLPH_RESUME=1
|
|
fi
|
|
fi
|
|
# 🔴 A POISONED TRANSCRIPT MUST NOT CRASH-LOOP. If the last start was under
|
|
# two minutes ago we are already in a restart loop, and continuing back into
|
|
# whatever killed us is the one thing guaranteed not to help. Start fresh and
|
|
# say so, rather than burning tokens on the same death repeatedly.
|
|
if [ "$SYLPH_RESUME" = "1" ] && [ -f "$SYLPH_STAMP" ]; then
|
|
_last=$(cat "$SYLPH_STAMP" 2>/dev/null || echo 0)
|
|
_now=$(date +%s)
|
|
if [ $((_now - _last)) -lt 120 ]; then
|
|
SYLPH_RESUME=0
|
|
log "restarted <120s after the last start — restart loop suspected;"
|
|
log " starting a FRESH session rather than continuing into the same death"
|
|
fi
|
|
fi
|
|
mkdir -p "$HOME/.claude" 2>/dev/null || true
|
|
date +%s > "$SYLPH_STAMP" 2>/dev/null || true
|
|
|
|
if [ "$SYLPH_RESUME" = "1" ] && [ "$#" -eq 1 ]; then
|
|
set -- "$1
|
|
|
|
⚠️ YOU WERE RESTARTED, and this session was resumed — your context is intact,
|
|
but the process that was running when it died is gone. Before anything else:
|
|
|
|
1. \`git -C /work status\`. Whatever you had in progress is still in the tree,
|
|
UNCOMMITTED. Commit it and \`push-work\` before starting anything new.
|
|
2. Any build, test or capture you had running did NOT finish. Do not read its
|
|
absence as a result.
|
|
3. The likeliest cause is an OOM kill — this container is capped at 6 GB.
|
|
\`CARGO_BUILD_JOBS\` is now set for you in the environment; do not raise it,
|
|
and prefer \`build-reborn test\` over a raw \`cargo test --release\`, which
|
|
bypasses the wrapper's job cap. That is exactly what killed the run on
|
|
2026-09-01."
|
|
log "resuming session ${SYLPH_SESSION%%-*}… with a restart notice"
|
|
fi
|
|
# Tell the gate-answering wrapper to stand down: a resumed session cannot
|
|
# show a first-run gate, and on 2026-09-04 its single-word patterns matched
|
|
# the /loop prompt itself and typed "2" and "1" into a live session.
|
|
[ "$SYLPH_RESUME" = "1" ] && export SYLPH_SKIP_GATES=1
|
|
[ "$SYLPH_RESUME" = "1" ] && set -- --resume "$SYLPH_SESSION" "$@"
|
|
|
|
# The flag the user asked for. It is refused under root, which is why this
|
|
# image runs as `agent`.
|
|
# Remote Control registers the session with your account so you can chat with
|
|
# the agent from claude.ai or your phone — the point of a detached run being
|
|
# that you are not sitting in front of it. The name is passed EXPLICITLY: the
|
|
# flag's value is optional, so a bare `--remote-control` would swallow the
|
|
# /loop prompt that follows as the session name.
|
|
if [ "${SYLPH_REMOTE:-1}" != "0" ]; then
|
|
set -- --remote-control "${SYLPH_REMOTE_NAME:-sylpheed-agent}" "$@"
|
|
log "Remote Control enabled as '${SYLPH_REMOTE_NAME:-sylpheed-agent}'"
|
|
fi
|
|
# claude-autonomous wraps `claude --dangerously-skip-permissions` in a pty and
|
|
# answers the one-time first-run gates. The Bypass Permissions disclaimer in
|
|
# particular has no config key that skips it, so unattended it hangs forever.
|
|
set -- claude-autonomous "$@"
|
|
log "starting Claude Code with --dangerously-skip-permissions in $(pwd)"
|
|
fi
|
|
|
|
exec "$@"
|