@rallycry/conveyor-agent 10.0.0 → 10.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-34NA4BCK.js → chunk-QKRPJ7DB.js} +1016 -71
- package/dist/chunk-QKRPJ7DB.js.map +1 -0
- package/dist/cli.js +174 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +105 -2
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/runtime/entrypoint.sh +627 -0
- package/dist/chunk-34NA4BCK.js.map +0 -1
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -eo pipefail
|
|
3
|
+
export HOME=/home/conveyor
|
|
4
|
+
export PATH="/home/conveyor/.bun/bin:${PATH}"
|
|
5
|
+
# Suppress the "new major version of npm available" notice — it writes to
|
|
6
|
+
# stderr during `npm view`, and if we ever capture stderr into a version
|
|
7
|
+
# string it corrupts the semver and breaks the agent install.
|
|
8
|
+
export NO_UPDATE_NOTIFIER=1
|
|
9
|
+
|
|
10
|
+
exec > >(tee -a /tmp/claudespace-bootstrap.log) 2>&1
|
|
11
|
+
|
|
12
|
+
start_workspace_sshd() {
|
|
13
|
+
local ssh_port="${CONVEYOR_WORKSPACE_SSH_PORT:-2222}"
|
|
14
|
+
mkdir -p /home/conveyor/.ssh
|
|
15
|
+
chmod 700 /home/conveyor/.ssh
|
|
16
|
+
touch /home/conveyor/.ssh/authorized_keys
|
|
17
|
+
chmod 600 /home/conveyor/.ssh/authorized_keys
|
|
18
|
+
if command -v sudo >/dev/null 2>&1 && [ -x /usr/sbin/sshd ]; then
|
|
19
|
+
sudo mkdir -p /run/sshd
|
|
20
|
+
# Bind all interfaces: preview-router tunnels in from outside the pod to the
|
|
21
|
+
# pod IP. Access is gated by the attach token + SSH public-key auth, not by
|
|
22
|
+
# network reachability (the pod has no public ingress).
|
|
23
|
+
if sudo /usr/sbin/sshd -o "ListenAddress=0.0.0.0" -o "Port=${ssh_port}"; then
|
|
24
|
+
echo "[pool] Workspace SSHD listening on 0.0.0.0:${ssh_port}"
|
|
25
|
+
else
|
|
26
|
+
echo "[pool] WARN: workspace SSHD failed to start"
|
|
27
|
+
fi
|
|
28
|
+
else
|
|
29
|
+
echo "[pool] WARN: workspace SSHD unavailable"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# ── Sidecar readiness is handled by the agent, not here ──
|
|
34
|
+
# The postgres / firebase wait moved into `conveyor-agent` (setup waitForSidecars())
|
|
35
|
+
# so it gates only the project setupCommand/startCommand and never the agent's
|
|
36
|
+
# thinking loop — even on-demand pods now launch the agent without blocking on the
|
|
37
|
+
# slow firebase emulator. The agent keeps the same per-target deadlines learned
|
|
38
|
+
# here (postgres 30s, firebase 60s). See:
|
|
39
|
+
# docs/superpowers/specs/2026-06-28-agent-launch-before-sidecar-setup-design.md
|
|
40
|
+
|
|
41
|
+
# ── Refresh agent runner to latest before launch (payload-independent) ──
|
|
42
|
+
# The image version can be stale vs. what the API expects (protocol/contract
|
|
43
|
+
# drift causes instant exit-1 crashes). Only install if the registry's `latest`
|
|
44
|
+
# is *strictly greater* than what's already in the image. If the image version
|
|
45
|
+
# is ahead (e.g. a dev pod image from an unreleased commit), never downgrade.
|
|
46
|
+
# `npm view` is a metadata-only fetch (~1s) vs. a full install (~30-60s). Hoisted
|
|
47
|
+
# into pre-warm so a claimed warm pod never pays this on the critical path.
|
|
48
|
+
refresh_agent_version() {
|
|
49
|
+
local _installed_agent _npm_view_stderr _npm_view_output _npm_view_rc
|
|
50
|
+
local _npm_view_err_content _latest_agent_raw _latest_agent _higher
|
|
51
|
+
_installed_agent=$(conveyor-agent --version 2>/dev/null | tr -d '[:space:]' || echo "")
|
|
52
|
+
# The image's /home/conveyor/.npmrc pins `@rallycry:registry=https://npm.pkg.github.com/`
|
|
53
|
+
# for other private @rallycry packages, but conveyor-agent itself is published
|
|
54
|
+
# to public npmjs.org. Override the scope on the command line so this lookup
|
|
55
|
+
# hits the right registry.
|
|
56
|
+
#
|
|
57
|
+
# Capture stdout and stderr SEPARATELY. Mixing them (2>&1) was a disaster:
|
|
58
|
+
# npm's update notifier (+ random future notices) write to stderr, and after
|
|
59
|
+
# `tr -d '[:space:]'` the version and the notice ran together into a garbage
|
|
60
|
+
# string like "7.0.12npmnoticenpmnotice..." — which then got fed to
|
|
61
|
+
# `npm install @rallycry/conveyor-agent@<garbage>`, which failed partway
|
|
62
|
+
# through and left the pod with NO conveyor-agent binary at all (crashloop).
|
|
63
|
+
# NO_UPDATE_NOTIFIER=1 is set at the top of this script as belt-and-suspenders,
|
|
64
|
+
# but don't rely on it.
|
|
65
|
+
#
|
|
66
|
+
# The if/else form on the assignment is required because `set -e` aborts on
|
|
67
|
+
# a failing command in assignment context, and a trailing `|| true` would
|
|
68
|
+
# mask the real exit code.
|
|
69
|
+
# The baked /home/conveyor/.npm cache dir is owned by root (npm ran as root
|
|
70
|
+
# during the image build), but the entrypoint runs as uid 1001 — so a non-sudo
|
|
71
|
+
# `npm view` fails with EACCES trying to mkdir its _cacache. Point every npm
|
|
72
|
+
# invocation here at a world-writable cache dir so the version lookup works.
|
|
73
|
+
local _npm_cache=/tmp/npm-cache
|
|
74
|
+
_npm_view_stderr=$(mktemp 2>/dev/null || echo "/tmp/npmview.$$.err")
|
|
75
|
+
if _npm_view_output=$(npm view --cache "$_npm_cache" \
|
|
76
|
+
--@rallycry:registry=https://registry.npmjs.org/ \
|
|
77
|
+
@rallycry/conveyor-agent version 2>"$_npm_view_stderr"); then
|
|
78
|
+
_npm_view_rc=0
|
|
79
|
+
else
|
|
80
|
+
_npm_view_rc=$?
|
|
81
|
+
fi
|
|
82
|
+
_npm_view_err_content=$(cat "$_npm_view_stderr" 2>/dev/null || echo "")
|
|
83
|
+
rm -f "$_npm_view_stderr" 2>/dev/null || true
|
|
84
|
+
_latest_agent_raw=$(printf '%s' "${_npm_view_output}" | tr -d '[:space:]')
|
|
85
|
+
# Validate the captured value looks like a semver (x.y.z with optional
|
|
86
|
+
# prerelease/build metadata). Anything else → treat as a failed lookup and
|
|
87
|
+
# keep the image version. This is the safety net that prevents us from ever
|
|
88
|
+
# running `npm install @rallycry/conveyor-agent@<corrupted>` again.
|
|
89
|
+
if [ $_npm_view_rc -eq 0 ] && [[ "${_latest_agent_raw}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.-]+)?$ ]]; then
|
|
90
|
+
_latest_agent="${_latest_agent_raw}"
|
|
91
|
+
else
|
|
92
|
+
_latest_agent=""
|
|
93
|
+
fi
|
|
94
|
+
if [ -z "${_latest_agent}" ]; then
|
|
95
|
+
echo "[pool] WARNING: failed to get valid semver from npm for @rallycry/conveyor-agent (rc=${_npm_view_rc}), keeping image version ${_installed_agent:-unknown}"
|
|
96
|
+
echo "[pool] npm view stdout: ${_npm_view_output}"
|
|
97
|
+
echo "[pool] npm view stderr: ${_npm_view_err_content}"
|
|
98
|
+
echo "[pool] raw value after whitespace strip: '${_latest_agent_raw}'"
|
|
99
|
+
echo "[pool] npm binary: $(command -v npm || echo 'not found'), node: $(command -v node || echo 'not found'), HOME=${HOME:-unset}"
|
|
100
|
+
elif [ -z "${_installed_agent}" ]; then
|
|
101
|
+
echo "[pool] No agent in image, installing @rallycry/conveyor-agent@${_latest_agent}..."
|
|
102
|
+
sudo npm install -g --cache "$_npm_cache" --silent "@rallycry/conveyor-agent@${_latest_agent}" 2>&1 \
|
|
103
|
+
|| echo "[pool] WARNING: agent install failed"
|
|
104
|
+
echo "[pool] Agent version: $(conveyor-agent --version 2>&1 || echo unknown)"
|
|
105
|
+
elif [ "${_installed_agent}" = "${_latest_agent}" ]; then
|
|
106
|
+
echo "[pool] Agent version: ${_installed_agent} (matches published, skipping install)"
|
|
107
|
+
else
|
|
108
|
+
# Semver compare via `sort -V`. Highest version is the last line.
|
|
109
|
+
_higher=$(printf '%s\n%s\n' "${_installed_agent}" "${_latest_agent}" | sort -V | tail -n1)
|
|
110
|
+
if [ "${_higher}" = "${_installed_agent}" ]; then
|
|
111
|
+
echo "[pool] Agent version: ${_installed_agent} (ahead of published ${_latest_agent}, skipping install)"
|
|
112
|
+
else
|
|
113
|
+
echo "[pool] Updating @rallycry/conveyor-agent ${_installed_agent} → ${_latest_agent}..."
|
|
114
|
+
sudo npm install -g --cache "$_npm_cache" --silent "@rallycry/conveyor-agent@${_latest_agent}" 2>&1 \
|
|
115
|
+
|| echo "[pool] WARNING: agent update failed, falling back to image version ${_installed_agent}"
|
|
116
|
+
echo "[pool] Agent version: $(conveyor-agent --version 2>&1 || echo unknown)"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
start_workspace_sshd
|
|
122
|
+
|
|
123
|
+
# ── Required env vars (injected by pod spec — v3: exactly these two) ──
|
|
124
|
+
: "${CONVEYOR_API_URL:?CONVEYOR_API_URL is required}"
|
|
125
|
+
: "${POD_BOOTSTRAP_TOKEN:?POD_BOOTSTRAP_TOKEN is required}"
|
|
126
|
+
|
|
127
|
+
# Pod name is the instance identifier
|
|
128
|
+
INSTANCE_NAME="${HOSTNAME}"
|
|
129
|
+
export CLAUDESPACE_NAME="${INSTANCE_NAME}"
|
|
130
|
+
|
|
131
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
# PRE-BIND PHASE — payload-independent work only. v3 pods do not have repo,
|
|
133
|
+
# GitHub, or task credentials until the bootstrap endpoint returns 200.
|
|
134
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
+
PREWARM_DEV_LOOP_PID=""
|
|
136
|
+
refresh_agent_version
|
|
137
|
+
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
# PULL-BASED BOOTSTRAP — poll until the reconciler binds this pod to a
|
|
140
|
+
# Workspace. 204 = still unbound; 200 = full bundle. No HMAC, no hostname
|
|
141
|
+
# derivation, no exec-push, no /tmp/assignment.json.
|
|
142
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
143
|
+
# Bash runs as PID 1: without a trap, SIGTERM is ignored during standby and
|
|
144
|
+
# every pool drain/scale-down rides the full terminationGracePeriod (180s) to
|
|
145
|
+
# SIGKILL. The agent-phase trap installed later replaces this one.
|
|
146
|
+
trap 'echo "[boot] SIGTERM during standby — exiting."; exit 0' TERM INT
|
|
147
|
+
echo "[boot] Entering standby — polling for bootstrap bind..."
|
|
148
|
+
POLL_COUNTER=0
|
|
149
|
+
BOOTSTRAP_JSON=""
|
|
150
|
+
while true; do
|
|
151
|
+
# The response body carries credentials — pre-create it owner-only so no
|
|
152
|
+
# window exists where another uid could read it.
|
|
153
|
+
rm -f /tmp/bootstrap-response.json
|
|
154
|
+
(umask 077 && touch /tmp/bootstrap-response.json)
|
|
155
|
+
# No `-f` on curl: with -f an HTTP-error response still prints the -w
|
|
156
|
+
# write-out AND exits 22, so `|| echo "000"` produced "401000" — the 401
|
|
157
|
+
# fail-fast branch below could never match and a deleted pod polled forever.
|
|
158
|
+
# Without -f curl exits 0 on any HTTP response and -w yields the clean
|
|
159
|
+
# status; the `|| echo "000"` fires only on pure network errors.
|
|
160
|
+
HTTP_STATUS=$(curl -s -o /tmp/bootstrap-response.json -w '%{http_code}' \
|
|
161
|
+
-H "Authorization: Bearer ${POD_BOOTSTRAP_TOKEN}" \
|
|
162
|
+
"${CONVEYOR_API_URL}/api/v3/pods/bootstrap" 2>/dev/null || echo "000")
|
|
163
|
+
|
|
164
|
+
if [ "${HTTP_STATUS}" = "200" ]; then
|
|
165
|
+
BOOTSTRAP_JSON=$(cat /tmp/bootstrap-response.json)
|
|
166
|
+
rm -f /tmp/bootstrap-response.json
|
|
167
|
+
echo "[boot] Bound — bootstrap bundle received!"
|
|
168
|
+
break
|
|
169
|
+
elif [ "${HTTP_STATUS}" = "401" ]; then
|
|
170
|
+
echo "[boot] ERROR: bootstrap token rejected (401) — pod identity invalid, exiting." >&2
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
rm -f /tmp/bootstrap-response.json
|
|
174
|
+
|
|
175
|
+
POLL_COUNTER=$((POLL_COUNTER + 1))
|
|
176
|
+
if [ $((POLL_COUNTER % 30)) -eq 0 ]; then
|
|
177
|
+
echo "[boot] Still waiting for bind (poll #${POLL_COUNTER})..."
|
|
178
|
+
fi
|
|
179
|
+
sleep 2
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
# `// empty` on every extraction: bare `.field` renders a missing/null field
|
|
183
|
+
# as the literal string "null", which then flows into git URLs and env vars.
|
|
184
|
+
export CONVEYOR_TASK_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.sessionJwt // empty')
|
|
185
|
+
export CONVEYOR_GITHUB_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.githubToken // empty')
|
|
186
|
+
REPO_OWNER=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.repoOwner // empty')
|
|
187
|
+
REPO_NAME=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.repoName // empty')
|
|
188
|
+
BRANCH=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.branch // empty')
|
|
189
|
+
BASE_BRANCH=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.baseBranch // empty')
|
|
190
|
+
CHECKOUT_REF=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.checkoutRef // empty')
|
|
191
|
+
export REPO_OWNER REPO_NAME BRANCH
|
|
192
|
+
|
|
193
|
+
# Decode non-secret session identity fields from the JWT payload so the
|
|
194
|
+
# existing agent startup contract remains unchanged.
|
|
195
|
+
SESSION_CLAIMS=$(node -e 'const t=process.argv[1].split(".")[1]||""; const s=t.replace(/-/g,"+").replace(/_/g,"/"); const p=s+"=".repeat((4-s.length%4)%4); process.stdout.write(Buffer.from(p,"base64").toString("utf8"));' "${CONVEYOR_TASK_TOKEN}" 2>/dev/null || echo "{}")
|
|
196
|
+
# CONVEYOR_TASK_ID is exported ONLY when the claim exists: a task-less PROJECT
|
|
197
|
+
# session JWT has no taskId claim, and the agent keys its project-runner mode
|
|
198
|
+
# off the var being ABSENT (never an empty-string export).
|
|
199
|
+
CONVEYOR_TASK_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.taskId // empty' 2>/dev/null || true)
|
|
200
|
+
if [ -n "${CONVEYOR_TASK_ID}" ]; then
|
|
201
|
+
export CONVEYOR_TASK_ID
|
|
202
|
+
else
|
|
203
|
+
unset CONVEYOR_TASK_ID
|
|
204
|
+
fi
|
|
205
|
+
# projectId claim is present only on task-less project session JWTs.
|
|
206
|
+
CONVEYOR_PROJECT_ID_CLAIM=$(echo "${SESSION_CLAIMS}" | jq -r '.projectId // empty' 2>/dev/null || true)
|
|
207
|
+
if [ -n "${CONVEYOR_PROJECT_ID_CLAIM}" ]; then
|
|
208
|
+
export CONVEYOR_PROJECT_ID="${CONVEYOR_PROJECT_ID_CLAIM}"
|
|
209
|
+
fi
|
|
210
|
+
export CONVEYOR_SESSION_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.sessionId // empty')
|
|
211
|
+
export CONVEYOR_WORKSPACE_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.workspaceId // empty')
|
|
212
|
+
SESSION_ROLE=$(echo "${SESSION_CLAIMS}" | jq -r '.role // empty')
|
|
213
|
+
unset SESSION_CLAIMS
|
|
214
|
+
if [ "${SESSION_ROLE}" = "reader" ]; then
|
|
215
|
+
export CONVEYOR_MODE="review"
|
|
216
|
+
elif [ -z "${CONVEYOR_TASK_ID:-}" ] && [ -n "${CONVEYOR_PROJECT_ID_CLAIM}" ]; then
|
|
217
|
+
# Task-less project pod: the agent boots the project runner in pm mode
|
|
218
|
+
# (see conveyor-agent setup/project-identity.ts — pm is required).
|
|
219
|
+
export CONVEYOR_MODE="pm"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# ── Export bundle env vars (secrets, OAuth tokens, project config) ──
|
|
223
|
+
ENV_KEYS=$(echo "${BOOTSTRAP_JSON}" | jq -r '.envVars // {} | keys[]' 2>/dev/null)
|
|
224
|
+
if [ -n "${ENV_KEYS}" ]; then
|
|
225
|
+
ENV_COUNT=0
|
|
226
|
+
while IFS= read -r key; do
|
|
227
|
+
# A bundle key that isn't a valid shell identifier would make `export`
|
|
228
|
+
# eval arbitrary content, and a handful of names would hijack the boot
|
|
229
|
+
# itself (PATH swaps every binary below; LD_PRELOAD injects code into
|
|
230
|
+
# them). Project env is user-supplied — validate, never trust.
|
|
231
|
+
if ! [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
232
|
+
echo "[boot] WARN: skipping invalid env key from bundle: '${key}'"
|
|
233
|
+
continue
|
|
234
|
+
fi
|
|
235
|
+
case "${key}" in
|
|
236
|
+
PATH|HOME|LD_PRELOAD|SHELL)
|
|
237
|
+
echo "[boot] WARN: skipping denylisted env key from bundle: '${key}'"
|
|
238
|
+
continue
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
241
|
+
value=$(echo "${BOOTSTRAP_JSON}" | jq -r --arg k "${key}" '.envVars[$k] // empty')
|
|
242
|
+
export "${key}=${value}"
|
|
243
|
+
ENV_COUNT=$((ENV_COUNT + 1))
|
|
244
|
+
done <<< "${ENV_KEYS}"
|
|
245
|
+
echo "[boot] Injected ${ENV_COUNT} env vars from bootstrap bundle"
|
|
246
|
+
fi
|
|
247
|
+
export ANTHROPIC_API_KEY=$(echo "${BOOTSTRAP_JSON}" | jq -r '.anthropicKey // empty')
|
|
248
|
+
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gcpToken // empty')
|
|
249
|
+
# WorkPreservation GCS tier (spec §Durability & snapshots): the upload URL is
|
|
250
|
+
# long-lived (used for the pod's whole lifetime of periodic captures + sleep
|
|
251
|
+
# finalize); the download URL is present only when a snapshot exists and is
|
|
252
|
+
# consumed once by restoreOnBoot.
|
|
253
|
+
CONVEYOR_SNAPSHOT_UPLOAD_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUploadUrl // empty')
|
|
254
|
+
CONVEYOR_SNAPSHOT_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUrl // empty')
|
|
255
|
+
export CONVEYOR_SNAPSHOT_UPLOAD_URL CONVEYOR_SNAPSHOT_URL
|
|
256
|
+
unset BOOTSTRAP_JSON
|
|
257
|
+
# POD_BOOTSTRAP_TOKEN is deliberately NOT unset: it is the credential-refresh
|
|
258
|
+
# key. The agent re-polls GET /api/v3/pods/bootstrap with it to swap in a
|
|
259
|
+
# fresh githubToken/sessionJwt in place (see agent-connection refresh path).
|
|
260
|
+
|
|
261
|
+
CONVEYOR_USER_ID="${CONVEYOR_USER_ID:-}"
|
|
262
|
+
CONVEYOR_PROJECT_ID_FROM_BUNDLE="${CONVEYOR_PROJECT_ID:-${PROJECT_ID:-}}"
|
|
263
|
+
export CONVEYOR_USER_ID
|
|
264
|
+
|
|
265
|
+
# ── Wire per-user Claude state from the GCS FUSE mount ──
|
|
266
|
+
# When the pod has /mnt/conveyor-users mounted (GCS FUSE CSI), symlink
|
|
267
|
+
# ~/.claude, ~/.claude.json, and ~/.config/claude to the user's subdir so
|
|
268
|
+
# Claude state persists across all of the user's codespaces without any
|
|
269
|
+
# tar/upload/download round-trip. Uses projectId if present, else falls
|
|
270
|
+
# back to PROJECT_ID from the pod env.
|
|
271
|
+
USER_HOME_MOUNT="/mnt/conveyor-users"
|
|
272
|
+
USER_HOME_PROJECT_ID="${CONVEYOR_PROJECT_ID_FROM_BUNDLE:-${PROJECT_ID:-}}"
|
|
273
|
+
if [ -n "${CONVEYOR_USER_ID}" ] && [ -n "${USER_HOME_PROJECT_ID}" ] && [ -d "${USER_HOME_MOUNT}" ]; then
|
|
274
|
+
USER_HOME_ROOT="${USER_HOME_MOUNT}/users/${CONVEYOR_USER_ID}/${USER_HOME_PROJECT_ID}"
|
|
275
|
+
echo "[pool] Linking Claude state to ${USER_HOME_ROOT}"
|
|
276
|
+
mkdir -p "${USER_HOME_ROOT}/.claude" "${USER_HOME_ROOT}/.config/claude" 2>/dev/null || true
|
|
277
|
+
# Seed .claude.json on first use. Claude Code treats a 0-byte file as
|
|
278
|
+
# corrupted ("Unexpected end of JSON input") and exits code 1, killing the
|
|
279
|
+
# very first query on any fresh user+project mount. We pre-complete onboarding
|
|
280
|
+
# and pick a default theme so the spawned Claude Code TUI doesn't stop at the
|
|
281
|
+
# first-run theme picker / login. Only seed when the file is missing/empty or a
|
|
282
|
+
# bare "{}" placeholder — never overwrite a real config carried over via the
|
|
283
|
+
# GCS-FUSE user-home mount from a prior pod session.
|
|
284
|
+
CLAUDE_JSON="${USER_HOME_ROOT}/.claude.json"
|
|
285
|
+
# Read defensively: on a first-time user+project mount the file does not
|
|
286
|
+
# exist yet. `< "${CLAUDE_JSON}"` on a missing file is a SHELL-level
|
|
287
|
+
# redirection error (the `2>/dev/null` only silences tr, not bash), which
|
|
288
|
+
# under `set -eo pipefail` aborts the whole entrypoint with exit 1 — the
|
|
289
|
+
# agent container then dies seconds after start and the pod is reaped
|
|
290
|
+
# ("deleted externally"). Guard on existence and rescue any read error so a
|
|
291
|
+
# missing/unreadable file falls through to the seed below instead of crashing.
|
|
292
|
+
if [ -f "${CLAUDE_JSON}" ]; then
|
|
293
|
+
CLAUDE_JSON_CURRENT="$(tr -d '[:space:]' < "${CLAUDE_JSON}" 2>/dev/null || true)"
|
|
294
|
+
else
|
|
295
|
+
CLAUDE_JSON_CURRENT=""
|
|
296
|
+
fi
|
|
297
|
+
if [ -z "${CLAUDE_JSON_CURRENT}" ] || [ "${CLAUDE_JSON_CURRENT}" = "{}" ]; then
|
|
298
|
+
echo '{"hasCompletedOnboarding":true,"theme":"dark"}' > "${CLAUDE_JSON}" 2>/dev/null || true
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# Replace any image defaults with live symlinks into the mount.
|
|
302
|
+
rm -rf /home/conveyor/.claude /home/conveyor/.claude.json /home/conveyor/.config/claude 2>/dev/null || true
|
|
303
|
+
mkdir -p /home/conveyor/.config 2>/dev/null || true
|
|
304
|
+
ln -sfn "${USER_HOME_ROOT}/.claude" /home/conveyor/.claude
|
|
305
|
+
ln -sfn "${USER_HOME_ROOT}/.claude.json" /home/conveyor/.claude.json
|
|
306
|
+
ln -sfn "${USER_HOME_ROOT}/.config/claude" /home/conveyor/.config/claude
|
|
307
|
+
else
|
|
308
|
+
echo "[pool] Skipping user-home symlink (userId='${CONVEYOR_USER_ID}', projectId='${USER_HOME_PROJECT_ID}', mount present: $([ -d "${USER_HOME_MOUNT}" ] && echo yes || echo no))"
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
312
|
+
# WORKSPACE GIT — moved OFF the pre-launch critical path (Claudespace v3).
|
|
313
|
+
#
|
|
314
|
+
# The agent is launched IMMEDIATELY (below) so the card lights up and setup
|
|
315
|
+
# output streams while git runs in the BACKGROUND. `prepare_workspace_git`
|
|
316
|
+
# does the full fetch/checkout/merge (task repo) then clones reference repos,
|
|
317
|
+
# and signals completion by writing exactly ONE marker file:
|
|
318
|
+
#
|
|
319
|
+
# GIT_READY_MARKER — task repo is up to date; the agent may spawn Claude.
|
|
320
|
+
# GIT_FAILED_MARKER — git preparation errored; the agent surfaces the error
|
|
321
|
+
# and shuts down WITHOUT operating on a broken/stale repo.
|
|
322
|
+
#
|
|
323
|
+
# This function runs backgrounded (`prepare_workspace_git &`). It therefore
|
|
324
|
+
# must NEVER `exit` (that only kills the subshell, leaving the agent to wait
|
|
325
|
+
# on a marker that never arrives) — every error path does
|
|
326
|
+
# `echo >&2; printf ... > "$GIT_FAILED_MARKER"; return 1` instead. The
|
|
327
|
+
# fail-loud-on-stale-image philosophy is preserved: a failure writes the failed
|
|
328
|
+
# marker (was: exit 1), which the agent treats as fatal.
|
|
329
|
+
#
|
|
330
|
+
# `set -e` interaction: fallible git commands keep the existing
|
|
331
|
+
# `if ! _out=$(...); then` guard so a non-zero rc reaches our marker write
|
|
332
|
+
# rather than aborting the subshell before it. Every exit path writes exactly
|
|
333
|
+
# one marker.
|
|
334
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
335
|
+
GIT_READY_MARKER="/workspaces/.conveyor-git-ready"
|
|
336
|
+
GIT_FAILED_MARKER="/workspaces/.conveyor-git-failed"
|
|
337
|
+
|
|
338
|
+
# Shared by both the pod-image branch and the repo-present-non-pod-image branch
|
|
339
|
+
# of prepare_workspace_git: refresh the remote token, fetch the base branch,
|
|
340
|
+
# fetch/checkout the task branch (creating it from base if it doesn't exist on
|
|
341
|
+
# origin yet), and merge latest base into it. On success the repo is left
|
|
342
|
+
# checked out on the task branch, up to date with base. On any failure it
|
|
343
|
+
# writes GIT_FAILED_MARKER and returns 1 — callers must not fall through to
|
|
344
|
+
# `: > "$GIT_READY_MARKER"` in that case.
|
|
345
|
+
sync_task_branch_to_repo() {
|
|
346
|
+
# Guarded so a set-url failure writes the failed marker instead of aborting
|
|
347
|
+
# the backgrounded subshell (which would leave the agent waiting forever).
|
|
348
|
+
if ! git -C repo remote set-url origin "https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" 2>/dev/null; then
|
|
349
|
+
echo "[pool] ERROR: remote set-url failed for pod-image repo" >&2
|
|
350
|
+
printf '%s' "pod-image remote set-url failed" > "$GIT_FAILED_MARKER"
|
|
351
|
+
return 1
|
|
352
|
+
fi
|
|
353
|
+
# Unshallow the baked repo if needed to prevent "refusing to merge unrelated histories"
|
|
354
|
+
if [ "$(git -C repo rev-parse --is-shallow-repository 2>/dev/null)" = "true" ]; then
|
|
355
|
+
echo "[pool] Unshallowing baked repo..."
|
|
356
|
+
if ! _unshallow_out=$(git -C repo fetch --unshallow origin 2>&1); then
|
|
357
|
+
echo "[pool] WARN: unshallow failed: ${_unshallow_out}" >&2
|
|
358
|
+
fi
|
|
359
|
+
fi
|
|
360
|
+
# Always fetch latest base so downstream steps (and the agent's
|
|
361
|
+
# syncWithBaseBranch) have an up-to-date origin/<base> to merge from.
|
|
362
|
+
DEV_BRANCH="${BASE_BRANCH}"
|
|
363
|
+
if ! _fetch_dev_out=$(git -C repo fetch origin "+refs/heads/${DEV_BRANCH}:refs/remotes/origin/${DEV_BRANCH}" 2>&1); then
|
|
364
|
+
echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [ -n "${CHECKOUT_REF}" ]; then
|
|
368
|
+
echo "[pool] Fetching checkout ref ${CHECKOUT_REF} for review branch ${BRANCH}..."
|
|
369
|
+
if ! _fetch_checkout_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
|
|
370
|
+
echo "[pool] ERROR: Repo fetch failed for checkout ref '${CHECKOUT_REF}': ${_fetch_checkout_out}" >&2
|
|
371
|
+
printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
372
|
+
return 1
|
|
373
|
+
fi
|
|
374
|
+
if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
|
|
375
|
+
echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
|
|
376
|
+
printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
377
|
+
return 1
|
|
378
|
+
fi
|
|
379
|
+
else
|
|
380
|
+
# Fetch the task branch into a local ref explicitly so we can check it out.
|
|
381
|
+
# If the branch doesn't exist on origin, create it from the base branch.
|
|
382
|
+
# Note: if BRANCH equals DEV_BRANCH (deferred-branch case), the fetch above
|
|
383
|
+
# already populated the ref and this is a no-op.
|
|
384
|
+
if ! _fetch_out=$(git -C repo fetch origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>&1); then
|
|
385
|
+
# Check if the error is specifically due to the remote ref not existing
|
|
386
|
+
if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
|
|
387
|
+
echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
|
|
388
|
+
# Create the branch locally from the base branch
|
|
389
|
+
if ! _create_out=$(git -C repo checkout -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
|
|
390
|
+
echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
|
|
391
|
+
printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
392
|
+
return 1
|
|
393
|
+
fi
|
|
394
|
+
# Push the new branch to origin with -u to set up tracking
|
|
395
|
+
if ! _push_out=$(git -C repo push -u origin "${BRANCH}" 2>&1); then
|
|
396
|
+
echo "[pool] ERROR: Failed to push new branch '${BRANCH}': ${_push_out}" >&2
|
|
397
|
+
printf '%s' "push new branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
398
|
+
return 1
|
|
399
|
+
fi
|
|
400
|
+
echo "[pool] Created and pushed new branch '${BRANCH}' from '${DEV_BRANCH}'"
|
|
401
|
+
else
|
|
402
|
+
# Other git error (auth, network, etc.) — fail loud as before
|
|
403
|
+
echo "[pool] ERROR: Repo fetch failed for branch '${BRANCH}': ${_fetch_out}" >&2
|
|
404
|
+
printf '%s' "fetch branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
405
|
+
return 1
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
408
|
+
# Use `checkout -B` to create or reset the local branch tracking origin.
|
|
409
|
+
# This leaves HEAD attached to a real branch (not detached) so `git status`,
|
|
410
|
+
# `git push`, and any tool that keys off branch state work correctly.
|
|
411
|
+
if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
|
|
412
|
+
echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
|
|
413
|
+
printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
414
|
+
return 1
|
|
415
|
+
fi
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# Merge latest dev into the task branch so the agent starts on up-to-date
|
|
419
|
+
# base code. Skip if the branch already contains origin/dev (fast-path)
|
|
420
|
+
# or if BRANCH is the dev branch itself.
|
|
421
|
+
if [ "${BRANCH}" != "${DEV_BRANCH}" ] && git -C repo rev-parse "origin/${DEV_BRANCH}" >/dev/null 2>&1; then
|
|
422
|
+
if git -C repo merge-base --is-ancestor "origin/${DEV_BRANCH}" HEAD; then
|
|
423
|
+
echo "[pool] Branch already up-to-date with origin/${DEV_BRANCH}"
|
|
424
|
+
elif ! _merge_out=$(git -C repo merge "origin/${DEV_BRANCH}" --no-edit 2>&1); then
|
|
425
|
+
echo "[pool] WARN: merge origin/${DEV_BRANCH} failed, aborting: ${_merge_out}" >&2
|
|
426
|
+
git -C repo merge --abort 2>/dev/null || true
|
|
427
|
+
else
|
|
428
|
+
echo "[pool] Merged origin/${DEV_BRANCH} into ${BRANCH}"
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
echo "[pool] Repo updated to ${BRANCH}@$(git -C repo rev-parse --short HEAD)"
|
|
432
|
+
return 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
prepare_workspace_git() {
|
|
436
|
+
# Update remote URL with fresh token, or clone if pre-clone failed
|
|
437
|
+
cd /workspaces
|
|
438
|
+
if [ "${CONVEYOR_POD_IMAGE}" = "1" ] && [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
|
|
439
|
+
# Pod image: repo already exists (and pre-warm already fetched origin/dev, so
|
|
440
|
+
# the fetch below is a near-instant fast-forward). Just refresh the remote with
|
|
441
|
+
# the assignment's fresh token and bring the task branch up to date.
|
|
442
|
+
# IMPORTANT: Do NOT silently fall through to the image snapshot when the
|
|
443
|
+
# requested branch can't be fetched/checked out. A stale image repo has
|
|
444
|
+
# bitten us before (old load-env.sh, old scripts, wrong deps) and the
|
|
445
|
+
# symptoms are very hard to diagnose from pod logs. Fail loud instead —
|
|
446
|
+
# now via the failed marker rather than exit 1.
|
|
447
|
+
echo "[pool] Pod image — updating repo to latest (branch=${BRANCH})..."
|
|
448
|
+
if ! sync_task_branch_to_repo; then
|
|
449
|
+
return 1
|
|
450
|
+
fi
|
|
451
|
+
# Task repo is ready HERE — release the gate. The agent (already launched)
|
|
452
|
+
# is polling for this marker before it spawns Claude / runs setup. Reference
|
|
453
|
+
# repos are cloned AFTER this so they never block Claude.
|
|
454
|
+
: > "$GIT_READY_MARKER"
|
|
455
|
+
elif [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
|
|
456
|
+
# Non-image but repo already present (e.g. a baked image running without
|
|
457
|
+
# CONVEYOR_POD_IMAGE set). Must NOT settle for a bare remote-URL refresh —
|
|
458
|
+
# that would leave the agent running on whatever branch the repo snapshot
|
|
459
|
+
# happened to be on (e.g. dev), silently committing/pushing to the wrong
|
|
460
|
+
# branch. Run the exact same fetch/checkout/merge flow as the pod-image
|
|
461
|
+
# branch so this path ends up in the same checked-out-on-task-branch state.
|
|
462
|
+
echo "[pool] Repo present (non-pod-image) — updating repo to latest (branch=${BRANCH})..."
|
|
463
|
+
if ! sync_task_branch_to_repo; then
|
|
464
|
+
return 1
|
|
465
|
+
fi
|
|
466
|
+
: > "$GIT_READY_MARKER"
|
|
467
|
+
elif [ -n "${CONVEYOR_GITHUB_TOKEN}" ] && [ -n "${REPO_OWNER}" ] && [ -n "${REPO_NAME}" ] && [ -n "${BRANCH}" ]; then
|
|
468
|
+
echo "[pool] Cloning repo post-assignment (pre-clone was missing)..."
|
|
469
|
+
# Guard each clone/fetch/checkout: under `set -e` a bare failing clone would
|
|
470
|
+
# abort the subshell before we can write the failed marker.
|
|
471
|
+
if [ -n "${CHECKOUT_REF}" ]; then
|
|
472
|
+
if ! _clone_out=$(git clone --depth 1 --single-branch --branch "${BASE_BRANCH}" \
|
|
473
|
+
"https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
|
|
474
|
+
repo 2>&1); then
|
|
475
|
+
echo "[pool] ERROR: post-assignment clone failed: ${_clone_out}" >&2
|
|
476
|
+
printf '%s' "post-assignment clone failed" > "$GIT_FAILED_MARKER"
|
|
477
|
+
return 1
|
|
478
|
+
fi
|
|
479
|
+
if ! _fetch_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
|
|
480
|
+
echo "[pool] ERROR: post-assignment fetch of ${CHECKOUT_REF} failed: ${_fetch_out}" >&2
|
|
481
|
+
printf '%s' "post-assignment fetch of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
482
|
+
return 1
|
|
483
|
+
fi
|
|
484
|
+
if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
|
|
485
|
+
echo "[pool] ERROR: post-assignment checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
|
|
486
|
+
printf '%s' "post-assignment checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
487
|
+
return 1
|
|
488
|
+
fi
|
|
489
|
+
else
|
|
490
|
+
# Clone the BASE branch — it always exists, unlike a brand-new task
|
|
491
|
+
# branch. A naive `clone --branch <task>` dies with "Remote branch not
|
|
492
|
+
# found" when the task branch has never been pushed to origin, which
|
|
493
|
+
# crash-loops the pod on every fresh task. sync_task_branch_to_repo below
|
|
494
|
+
# resolves/creates ${BRANCH} from the clone identically to the pod-image
|
|
495
|
+
# path. Full depth (no --depth 1) so its base-merge never hits "refusing
|
|
496
|
+
# to merge unrelated histories".
|
|
497
|
+
if ! _clone_out=$(git clone --single-branch --branch "${BASE_BRANCH}" \
|
|
498
|
+
"https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
|
|
499
|
+
repo 2>&1); then
|
|
500
|
+
echo "[pool] ERROR: post-assignment clone of base '${BASE_BRANCH}' failed: ${_clone_out}" >&2
|
|
501
|
+
printf '%s' "post-assignment clone failed" > "$GIT_FAILED_MARKER"
|
|
502
|
+
return 1
|
|
503
|
+
fi
|
|
504
|
+
if ! sync_task_branch_to_repo; then
|
|
505
|
+
return 1
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
: > "$GIT_READY_MARKER"
|
|
509
|
+
else
|
|
510
|
+
# No git plan (e.g. task-less pod, or no token/repo). Nothing to prepare —
|
|
511
|
+
# the repo dir already exists (created before launch) and the agent's own
|
|
512
|
+
# git-sync fallback (guarded by CONVEYOR_GIT_READY) never runs on this pod
|
|
513
|
+
# path anyway. Signal ready so the agent doesn't wait out the timeout.
|
|
514
|
+
echo "[pool] No git plan to prepare — marking git ready."
|
|
515
|
+
: > "$GIT_READY_MARKER"
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# ── Clone reference repos into /workspaces/references (best-effort) ──
|
|
519
|
+
# Populated from REFERENCE_REPOS_JSON (injected via envVars by
|
|
520
|
+
# injectReferenceRepos). Each repo is shallow-cloned as read-only context for
|
|
521
|
+
# the agent. Failures are non-fatal — the task must still proceed even if a
|
|
522
|
+
# reference project's GitHub App is uninstalled or the token mint failed.
|
|
523
|
+
# This runs AFTER the ready marker: references are supplementary context and
|
|
524
|
+
# must NOT block Claude from spawning.
|
|
525
|
+
if [ -n "${REFERENCE_REPOS_JSON:-}" ]; then
|
|
526
|
+
mkdir -p /workspaces/references
|
|
527
|
+
echo "${REFERENCE_REPOS_JSON}" | jq -c '.[]' 2>/dev/null | while IFS= read -r ref; do
|
|
528
|
+
REF_SLUG=$(echo "${ref}" | jq -r '.slug')
|
|
529
|
+
REF_OWNER=$(echo "${ref}" | jq -r '.owner')
|
|
530
|
+
REF_NAME=$(echo "${ref}" | jq -r '.name')
|
|
531
|
+
REF_BRANCH=$(echo "${ref}" | jq -r '.branch // "main"')
|
|
532
|
+
REF_TOKEN=$(echo "${ref}" | jq -r '.token // empty')
|
|
533
|
+
if [ -z "${REF_TOKEN}" ] || [ -z "${REF_SLUG}" ] || [ -z "${REF_OWNER}" ] || [ -z "${REF_NAME}" ]; then
|
|
534
|
+
continue
|
|
535
|
+
fi
|
|
536
|
+
if [ -d "/workspaces/references/${REF_SLUG}/.git" ]; then
|
|
537
|
+
continue
|
|
538
|
+
fi
|
|
539
|
+
if git clone --depth 1 --single-branch --branch "${REF_BRANCH}" \
|
|
540
|
+
"https://x-access-token:${REF_TOKEN}@github.com/${REF_OWNER}/${REF_NAME}.git" \
|
|
541
|
+
"/workspaces/references/${REF_SLUG}" 2>/dev/null; then
|
|
542
|
+
# Strip the token from the cloned remote so it never surfaces via
|
|
543
|
+
# `git remote -v` when the agent inspects the reference repo.
|
|
544
|
+
git -C "/workspaces/references/${REF_SLUG}" remote set-url origin \
|
|
545
|
+
"https://github.com/${REF_OWNER}/${REF_NAME}.git" 2>/dev/null || true
|
|
546
|
+
echo "[pool] cloned reference ${REF_SLUG} (${REF_OWNER}/${REF_NAME}@${REF_BRANCH})"
|
|
547
|
+
else
|
|
548
|
+
echo "[pool] WARN: reference clone failed: ${REF_SLUG}"
|
|
549
|
+
fi
|
|
550
|
+
done
|
|
551
|
+
unset REFERENCE_REPOS_JSON
|
|
552
|
+
fi
|
|
553
|
+
return 0
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# Preview traffic is now proxied directly via k8s API pod proxy —
|
|
557
|
+
# no tunnel client needed. The API routes subdomain requests through
|
|
558
|
+
# the k8s API to reach the pod's ports directly.
|
|
559
|
+
|
|
560
|
+
# ── Clear stale markers, then background git and launch the agent early ──
|
|
561
|
+
# A leftover ready marker from a PRIOR pod on the baked image would be
|
|
562
|
+
# catastrophic: the agent would spawn Claude before THIS pod's git runs. Clear
|
|
563
|
+
# both before starting the background prep.
|
|
564
|
+
rm -f "$GIT_READY_MARKER" "$GIT_FAILED_MARKER"
|
|
565
|
+
# Ensure the repo dir exists for launch on BOTH paths (the background clone
|
|
566
|
+
# populates an empty dir on the non-image path; the pod-image path already has
|
|
567
|
+
# repo/.git).
|
|
568
|
+
mkdir -p /workspaces/repo
|
|
569
|
+
|
|
570
|
+
# Run the full git prep in the background so the agent can connect (card lights
|
|
571
|
+
# up) and stream setup output while git finishes.
|
|
572
|
+
prepare_workspace_git &
|
|
573
|
+
|
|
574
|
+
# Signal to the agent that bash owns the git block (skip the agent's OWN
|
|
575
|
+
# git-sync fallback) and that it must await the ready marker before spawning
|
|
576
|
+
# Claude / running setup.
|
|
577
|
+
export CONVEYOR_GIT_READY=1
|
|
578
|
+
export CONVEYOR_GIT_READY_MARKER="$GIT_READY_MARKER"
|
|
579
|
+
export CONVEYOR_GIT_FAILED_MARKER="$GIT_FAILED_MARKER"
|
|
580
|
+
# Target the repo regardless of when the background clone lands.
|
|
581
|
+
export CONVEYOR_WORKSPACE=/workspaces/repo
|
|
582
|
+
|
|
583
|
+
# Launch agent — exit-code-aware restart loop.
|
|
584
|
+
# Exit 0 = clean shutdown (idle timeout, task complete) — pod dies.
|
|
585
|
+
# Non-zero = crash — retry after a brief pause.
|
|
586
|
+
cd /workspaces/repo
|
|
587
|
+
echo "[pool] Launching agent..."
|
|
588
|
+
set +e
|
|
589
|
+
# Belt-and-braces alongside the pod's preStop hook (which pkills the agent
|
|
590
|
+
# directly because bash as PID 1 does not forward signals): if a SIGTERM does
|
|
591
|
+
# reach this shell (manual kill, future spec changes), forward it to the agent
|
|
592
|
+
# so flushGitOnShutdown still runs, then exit cleanly within the grace period.
|
|
593
|
+
trap 'echo "[pool] SIGTERM received, forwarding to agent..."; pkill -TERM -f conveyor-agent; wait; exit 0' TERM
|
|
594
|
+
# Bounded retry for a failed git prep. Without this, a transient git-prep
|
|
595
|
+
# failure writes GIT_FAILED_MARKER once and the agent's git gate then exits
|
|
596
|
+
# nonzero on every single relaunch forever (the marker never clears itself),
|
|
597
|
+
# crashlooping the pod every ~10s. Retry git prep itself, up to a small cap,
|
|
598
|
+
# before each relaunch; once the cap is exhausted stop hammering and fall
|
|
599
|
+
# back to the existing behavior (agent surfaces the failure) with a wider
|
|
600
|
+
# sleep so the pod idles instead of spinning.
|
|
601
|
+
GIT_PREP_MAX_RETRIES=3
|
|
602
|
+
_git_prep_attempts=0
|
|
603
|
+
while true; do
|
|
604
|
+
if [ -f "$GIT_FAILED_MARKER" ]; then
|
|
605
|
+
if [ "$_git_prep_attempts" -lt "$GIT_PREP_MAX_RETRIES" ]; then
|
|
606
|
+
_git_prep_attempts=$((_git_prep_attempts + 1))
|
|
607
|
+
echo "[pool] git prep previously failed — retrying (attempt ${_git_prep_attempts}/${GIT_PREP_MAX_RETRIES})..."
|
|
608
|
+
rm -f "$GIT_READY_MARKER" "$GIT_FAILED_MARKER"
|
|
609
|
+
prepare_workspace_git &
|
|
610
|
+
else
|
|
611
|
+
echo "[pool] git prep failed ${GIT_PREP_MAX_RETRIES} times — giving up on retries, letting agent surface the failure."
|
|
612
|
+
fi
|
|
613
|
+
fi
|
|
614
|
+
conveyor-agent 2>&1 | tee -a /tmp/claudespace-agent.log
|
|
615
|
+
_exit_code=${PIPESTATUS[0]}
|
|
616
|
+
if [ "$_exit_code" -eq 0 ]; then
|
|
617
|
+
echo "[pool] agent exited cleanly (code 0), shutting down pod."
|
|
618
|
+
exit 0
|
|
619
|
+
fi
|
|
620
|
+
if [ -f "$GIT_FAILED_MARKER" ] && [ "$_git_prep_attempts" -ge "$GIT_PREP_MAX_RETRIES" ]; then
|
|
621
|
+
echo "[pool] agent crashed (code $_exit_code) after git prep exhausted retries, backing off (60s)..."
|
|
622
|
+
sleep 60
|
|
623
|
+
else
|
|
624
|
+
echo "[pool] agent crashed (code $_exit_code), retrying in 10s..."
|
|
625
|
+
sleep 10
|
|
626
|
+
fi
|
|
627
|
+
done
|