@rallycry/conveyor-agent 10.13.14 → 10.13.16
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/boot-NMXFM4GK.js +1268 -0
- package/dist/boot-NMXFM4GK.js.map +1 -0
- package/dist/{server-TVIXWCAI.js → chunk-5UYMDSAE.js} +8 -3
- package/dist/chunk-5UYMDSAE.js.map +1 -0
- package/dist/chunk-6GS5ADIY.js +11 -0
- package/dist/chunk-6GS5ADIY.js.map +1 -0
- package/dist/{chunk-RSIW2UCR.js → chunk-JG54UJ6V.js} +2 -2
- package/dist/{chunk-VKZ5W2VO.js → chunk-JIGG755T.js} +1 -1
- package/dist/chunk-JIGG755T.js.map +1 -0
- package/dist/{chunk-J4UH5TOO.js → chunk-OW2TIDWZ.js} +71 -114
- package/dist/chunk-OW2TIDWZ.js.map +1 -0
- package/dist/{chunk-LZM5OKAC.js → chunk-UDSRAHKP.js} +19 -2
- package/dist/chunk-UDSRAHKP.js.map +1 -0
- package/dist/chunk-UZTJJD7Y.js +254 -0
- package/dist/chunk-UZTJJD7Y.js.map +1 -0
- package/dist/cli.js +30 -47
- package/dist/cli.js.map +1 -1
- package/dist/{client-VQIBGPE6.js → client-BU4XA7CV.js} +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{protocol-IHRTO5C4.js → protocol-QLVS5W6O.js} +2 -2
- package/dist/server-CO5O7G4U.js +9 -0
- package/dist/server-CO5O7G4U.js.map +1 -0
- package/package.json +1 -1
- package/runtime/entrypoint.sh +8 -975
- package/dist/chunk-J4UH5TOO.js.map +0 -1
- package/dist/chunk-LZM5OKAC.js.map +0 -1
- package/dist/chunk-VKZ5W2VO.js.map +0 -1
- package/dist/server-TVIXWCAI.js.map +0 -1
- /package/dist/{chunk-RSIW2UCR.js.map → chunk-JG54UJ6V.js.map} +0 -0
- /package/dist/{client-VQIBGPE6.js.map → client-BU4XA7CV.js.map} +0 -0
- /package/dist/{protocol-IHRTO5C4.js.map → protocol-QLVS5W6O.js.map} +0 -0
package/runtime/entrypoint.sh
CHANGED
|
@@ -1,978 +1,11 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# vnext shim — ALL boot logic lives in `conveyor-agent boot`
|
|
3
|
+
# (packages/conveyor-agent/src/boot/). Keep this file dumb: env floor,
|
|
4
|
+
# best-effort self-update preflight, exec. The preflight runs baked code and
|
|
5
|
+
# only installs; the exec picks up whichever global bin is now current.
|
|
6
|
+
set -eu
|
|
3
7
|
export HOME=/home/conveyor
|
|
4
8
|
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
9
|
export NO_UPDATE_NOTIFIER=1
|
|
9
|
-
|
|
10
|
-
exec
|
|
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 separate per-target deadlines here
|
|
38
|
-
# (postgres 30s, firebase 60s).
|
|
39
|
-
|
|
40
|
-
# ── Refresh agent runner to latest before launch (payload-independent) ──
|
|
41
|
-
# The image version can be stale vs. what the API expects (protocol/contract
|
|
42
|
-
# drift causes instant exit-1 crashes). Only install if the registry's `latest`
|
|
43
|
-
# is *strictly greater* than what's already in the image. If the image version
|
|
44
|
-
# is ahead (e.g. a dev pod image from an unreleased commit), never downgrade.
|
|
45
|
-
# `npm view` is a metadata-only fetch (~1s) vs. a full install (~30-60s). Hoisted
|
|
46
|
-
# into pre-warm so a claimed warm pod never pays this on the critical path.
|
|
47
|
-
refresh_agent_version() {
|
|
48
|
-
local _installed_agent _npm_view_stderr _npm_view_output _npm_view_rc
|
|
49
|
-
local _npm_view_err_content _latest_agent_raw _latest_agent _higher
|
|
50
|
-
_installed_agent=$(conveyor-agent --version 2>/dev/null | tr -d '[:space:]' || echo "")
|
|
51
|
-
# The image's /home/conveyor/.npmrc pins `@rallycry:registry=https://npm.pkg.github.com/`
|
|
52
|
-
# for other private @rallycry packages, but conveyor-agent itself is published
|
|
53
|
-
# to public npmjs.org. Override the scope on the command line so this lookup
|
|
54
|
-
# hits the right registry.
|
|
55
|
-
#
|
|
56
|
-
# Capture stdout and stderr SEPARATELY. Mixing them (2>&1) was a disaster:
|
|
57
|
-
# npm's update notifier (+ random future notices) write to stderr, and after
|
|
58
|
-
# `tr -d '[:space:]'` the version and the notice ran together into a garbage
|
|
59
|
-
# string like "7.0.12npmnoticenpmnotice..." — which then got fed to
|
|
60
|
-
# `npm install @rallycry/conveyor-agent@<garbage>`, which failed partway
|
|
61
|
-
# through and left the pod with NO conveyor-agent binary at all (crashloop).
|
|
62
|
-
# NO_UPDATE_NOTIFIER=1 is set at the top of this script as belt-and-suspenders,
|
|
63
|
-
# but don't rely on it.
|
|
64
|
-
#
|
|
65
|
-
# The if/else form on the assignment is required because `set -e` aborts on
|
|
66
|
-
# a failing command in assignment context, and a trailing `|| true` would
|
|
67
|
-
# mask the real exit code.
|
|
68
|
-
# The baked /home/conveyor/.npm cache dir is owned by root (npm ran as root
|
|
69
|
-
# during the image build), but the entrypoint runs as uid 1001 — so a non-sudo
|
|
70
|
-
# `npm view` fails with EACCES trying to mkdir its _cacache. Point every npm
|
|
71
|
-
# invocation here at a world-writable cache dir so the version lookup works.
|
|
72
|
-
local _npm_cache=/tmp/npm-cache
|
|
73
|
-
_npm_view_stderr=$(mktemp 2>/dev/null || echo "/tmp/npmview.$$.err")
|
|
74
|
-
if _npm_view_output=$(npm view --cache "$_npm_cache" \
|
|
75
|
-
--@rallycry:registry=https://registry.npmjs.org/ \
|
|
76
|
-
@rallycry/conveyor-agent version 2>"$_npm_view_stderr"); then
|
|
77
|
-
_npm_view_rc=0
|
|
78
|
-
else
|
|
79
|
-
_npm_view_rc=$?
|
|
80
|
-
fi
|
|
81
|
-
_npm_view_err_content=$(cat "$_npm_view_stderr" 2>/dev/null || echo "")
|
|
82
|
-
rm -f "$_npm_view_stderr" 2>/dev/null || true
|
|
83
|
-
_latest_agent_raw=$(printf '%s' "${_npm_view_output}" | tr -d '[:space:]')
|
|
84
|
-
# Validate the captured value looks like a semver (x.y.z with optional
|
|
85
|
-
# prerelease/build metadata). Anything else → treat as a failed lookup and
|
|
86
|
-
# keep the image version. This is the safety net that prevents us from ever
|
|
87
|
-
# running `npm install @rallycry/conveyor-agent@<corrupted>` again.
|
|
88
|
-
if [ $_npm_view_rc -eq 0 ] && [[ "${_latest_agent_raw}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.-]+)?$ ]]; then
|
|
89
|
-
_latest_agent="${_latest_agent_raw}"
|
|
90
|
-
else
|
|
91
|
-
_latest_agent=""
|
|
92
|
-
fi
|
|
93
|
-
if [ -z "${_latest_agent}" ]; then
|
|
94
|
-
echo "[pool] WARNING: failed to get valid semver from npm for @rallycry/conveyor-agent (rc=${_npm_view_rc}), keeping image version ${_installed_agent:-unknown}"
|
|
95
|
-
echo "[pool] npm view stdout: ${_npm_view_output}"
|
|
96
|
-
echo "[pool] npm view stderr: ${_npm_view_err_content}"
|
|
97
|
-
echo "[pool] raw value after whitespace strip: '${_latest_agent_raw}'"
|
|
98
|
-
echo "[pool] npm binary: $(command -v npm || echo 'not found'), node: $(command -v node || echo 'not found'), HOME=${HOME:-unset}"
|
|
99
|
-
elif [ -z "${_installed_agent}" ]; then
|
|
100
|
-
echo "[pool] No agent in image, installing @rallycry/conveyor-agent@${_latest_agent}..."
|
|
101
|
-
sudo npm install -g --cache "$_npm_cache" --silent "@rallycry/conveyor-agent@${_latest_agent}" 2>&1 \
|
|
102
|
-
|| echo "[pool] WARNING: agent install failed"
|
|
103
|
-
echo "[pool] Agent version: $(conveyor-agent --version 2>&1 || echo unknown)"
|
|
104
|
-
elif [ "${_installed_agent}" = "${_latest_agent}" ]; then
|
|
105
|
-
echo "[pool] Agent version: ${_installed_agent} (matches published, skipping install)"
|
|
106
|
-
else
|
|
107
|
-
# Semver compare via `sort -V`. Highest version is the last line.
|
|
108
|
-
_higher=$(printf '%s\n%s\n' "${_installed_agent}" "${_latest_agent}" | sort -V | tail -n1)
|
|
109
|
-
if [ "${_higher}" = "${_installed_agent}" ]; then
|
|
110
|
-
echo "[pool] Agent version: ${_installed_agent} (ahead of published ${_latest_agent}, skipping install)"
|
|
111
|
-
else
|
|
112
|
-
echo "[pool] Updating @rallycry/conveyor-agent ${_installed_agent} → ${_latest_agent}..."
|
|
113
|
-
sudo npm install -g --cache "$_npm_cache" --silent "@rallycry/conveyor-agent@${_latest_agent}" 2>&1 \
|
|
114
|
-
|| echo "[pool] WARNING: agent update failed, falling back to image version ${_installed_agent}"
|
|
115
|
-
echo "[pool] Agent version: $(conveyor-agent --version 2>&1 || echo unknown)"
|
|
116
|
-
fi
|
|
117
|
-
fi
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# ── Reclaim any root-owned $HOME config/state entries (bake defense-in-depth) ──
|
|
121
|
-
# Bake steps that run as root while ENV HOME points at /home/conveyor can leave
|
|
122
|
-
# root-owned entries under ~/.config / ~/.local (a uv install receipt in
|
|
123
|
-
# ~/.config once bricked every pod boot fleet-wide). This reclaims them so a
|
|
124
|
-
# baked ownership slip can't EACCES the entrypoint under `set -e`.
|
|
125
|
-
#
|
|
126
|
-
# CONDITIONAL, not `chown -R`: a `find … ! -user conveyor` walk is stat-only when
|
|
127
|
-
# the tree is already clean (the common case since the bake-side `HOME=/root`
|
|
128
|
-
# fix in 9815c8915 — current images carry ZERO root-owned files here), so it
|
|
129
|
-
# triggers no overlayfs copy-up. The former unconditional `chown -R` copied up
|
|
130
|
-
# ALL ~3.9k baked files (~63MB, dominated by ~/.config/opencode's 3,652 files)
|
|
131
|
-
# on EVERY boot even when nothing was root-owned — ~16s of I/O-bound
|
|
132
|
-
# uninterruptible-disk-sleep that reclaimed nothing, and the true owner of the
|
|
133
|
-
# "GCS-FUSE" boot gap. Runs in the PRE-BIND phase below (payload-independent, off
|
|
134
|
-
# the time-to-agent critical path) and before any mount symlinks exist, so it
|
|
135
|
-
# never follows into the FUSE mount. See
|
|
136
|
-
# docs/investigations/gcs-fuse-user-home-boot-cost-2026-07-15.md.
|
|
137
|
-
reclaim_home_ownership() {
|
|
138
|
-
local d
|
|
139
|
-
for d in /home/conveyor/.config /home/conveyor/.local; do
|
|
140
|
-
[ -d "${d}" ] || continue
|
|
141
|
-
sudo -n find "${d}" ! -user conveyor -exec chown conveyor:conveyor {} + 2>/dev/null || true
|
|
142
|
-
done
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# ── Container role (workbench split) ──
|
|
146
|
-
# Split-mode pods run TWO app containers from this same entrypoint:
|
|
147
|
-
# agent — supervisor + conveyor-agent only (protected, restartPolicy
|
|
148
|
-
# Never semantics); no repo, no sshd, no git prep.
|
|
149
|
-
# workbench — the workspace: git prep, sshd, claude + all workloads behind
|
|
150
|
-
# the launcher daemon (native sidecar, restartPolicy Always).
|
|
151
|
-
# Unset = today's monolith container; every guard below must be a no-op then.
|
|
152
|
-
CONTAINER_ROLE="${CONVEYOR_CONTAINER_ROLE:-}"
|
|
153
|
-
|
|
154
|
-
# Split-mode pods: SSH attach lands in the WORKBENCH container (the workspace
|
|
155
|
-
# lives there); the agent container skips sshd.
|
|
156
|
-
if [ "${CONTAINER_ROLE}" != "agent" ]; then
|
|
157
|
-
start_workspace_sshd
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
# ── Required env vars (injected by pod spec — v3: exactly these two) ──
|
|
161
|
-
: "${CONVEYOR_API_URL:?CONVEYOR_API_URL is required}"
|
|
162
|
-
: "${POD_BOOTSTRAP_TOKEN:?POD_BOOTSTRAP_TOKEN is required}"
|
|
163
|
-
|
|
164
|
-
# Pod name is the instance identifier
|
|
165
|
-
INSTANCE_NAME="${HOSTNAME}"
|
|
166
|
-
export CLAUDESPACE_NAME="${INSTANCE_NAME}"
|
|
167
|
-
|
|
168
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
169
|
-
# PRE-BIND PHASE — payload-independent work only. v3 pods do not have repo,
|
|
170
|
-
# GitHub, or task credentials until the bootstrap endpoint returns 200.
|
|
171
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
-
PREWARM_DEV_LOOP_PID=""
|
|
173
|
-
refresh_agent_version
|
|
174
|
-
# Reclaim root-owned $HOME entries here, during standby, so the common (clean)
|
|
175
|
-
# case costs a stat-only walk off the critical path instead of a ~16s copy-up
|
|
176
|
-
# `chown -R` after the bundle arrives.
|
|
177
|
-
reclaim_home_ownership
|
|
178
|
-
|
|
179
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
180
|
-
# PULL-BASED BOOTSTRAP — poll until the reconciler binds this pod to a
|
|
181
|
-
# Workspace. 204 = still unbound; 200 = full bundle.
|
|
182
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
-
# Bash runs as PID 1: without a trap, SIGTERM is ignored during standby and
|
|
184
|
-
# every pool drain/scale-down rides the full terminationGracePeriod (180s) to
|
|
185
|
-
# SIGKILL. The agent-phase trap installed later replaces this one.
|
|
186
|
-
trap 'echo "[boot] SIGTERM during standby — exiting."; exit 0' TERM INT
|
|
187
|
-
echo "[boot] Entering standby — polling for bootstrap bind..."
|
|
188
|
-
POLL_COUNTER=0
|
|
189
|
-
BOOTSTRAP_JSON=""
|
|
190
|
-
while true; do
|
|
191
|
-
# The response body carries credentials — pre-create it owner-only so no
|
|
192
|
-
# window exists where another uid could read it.
|
|
193
|
-
rm -f /tmp/bootstrap-response.json
|
|
194
|
-
(umask 077 && touch /tmp/bootstrap-response.json)
|
|
195
|
-
# No `-f` on curl: with -f an HTTP-error response still prints the -w
|
|
196
|
-
# write-out AND exits 22, so `|| echo "000"` produced "401000" — the 401
|
|
197
|
-
# fail-fast branch below could never match and a deleted pod polled forever.
|
|
198
|
-
# Without -f curl exits 0 on any HTTP response and -w yields the clean
|
|
199
|
-
# status; the `|| echo "000"` fires only on pure network errors.
|
|
200
|
-
HTTP_STATUS=$(curl -s -o /tmp/bootstrap-response.json -w '%{http_code}' \
|
|
201
|
-
-H "Authorization: Bearer ${POD_BOOTSTRAP_TOKEN}" \
|
|
202
|
-
"${CONVEYOR_API_URL}/api/v3/pods/bootstrap" 2>/dev/null || echo "000")
|
|
203
|
-
|
|
204
|
-
if [ "${HTTP_STATUS}" = "200" ]; then
|
|
205
|
-
BOOTSTRAP_JSON=$(cat /tmp/bootstrap-response.json)
|
|
206
|
-
rm -f /tmp/bootstrap-response.json
|
|
207
|
-
echo "[boot] Bound — bootstrap bundle received!"
|
|
208
|
-
break
|
|
209
|
-
elif [ "${HTTP_STATUS}" = "401" ]; then
|
|
210
|
-
echo "[boot] ERROR: bootstrap token rejected (401) — pod identity invalid, exiting." >&2
|
|
211
|
-
exit 1
|
|
212
|
-
fi
|
|
213
|
-
rm -f /tmp/bootstrap-response.json
|
|
214
|
-
|
|
215
|
-
POLL_COUNTER=$((POLL_COUNTER + 1))
|
|
216
|
-
if [ $((POLL_COUNTER % 30)) -eq 0 ]; then
|
|
217
|
-
echo "[boot] Still waiting for bind (poll #${POLL_COUNTER})..."
|
|
218
|
-
fi
|
|
219
|
-
sleep 2
|
|
220
|
-
done
|
|
221
|
-
|
|
222
|
-
# `// empty` on every extraction: bare `.field` renders a missing/null field
|
|
223
|
-
# as the literal string "null", which then flows into git URLs and env vars.
|
|
224
|
-
export CONVEYOR_TASK_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.sessionJwt // empty')
|
|
225
|
-
export CONVEYOR_GITHUB_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.githubToken // empty')
|
|
226
|
-
# gh CLI auth (baked into the base image). GH_TOKEN (not GITHUB_TOKEN — that
|
|
227
|
-
# name leaks into too many third-party tools) points gh at the same role-scoped
|
|
228
|
-
# installation token git uses. Caveat: installation tokens live ~1h and the
|
|
229
|
-
# agent's refresh path updates the git remote URL, not this env — a long-lived
|
|
230
|
-
# session's gh calls can 401 after expiry; agents should treat that as
|
|
231
|
-
# "re-check via MCP tools", not an auth bug to debug.
|
|
232
|
-
export GH_TOKEN="${CONVEYOR_GITHUB_TOKEN}"
|
|
233
|
-
REPO_OWNER=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.repoOwner // empty')
|
|
234
|
-
REPO_NAME=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.repoName // empty')
|
|
235
|
-
BRANCH=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.branch // empty')
|
|
236
|
-
BASE_BRANCH=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.baseBranch // empty')
|
|
237
|
-
CHECKOUT_REF=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gitPlan.checkoutRef // empty')
|
|
238
|
-
export REPO_OWNER REPO_NAME BRANCH
|
|
239
|
-
|
|
240
|
-
# Decode non-secret session identity fields from the JWT payload so the
|
|
241
|
-
# existing agent startup contract remains unchanged.
|
|
242
|
-
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 "{}")
|
|
243
|
-
# CONVEYOR_TASK_ID is exported ONLY when the claim exists: a task-less PROJECT
|
|
244
|
-
# session JWT has no taskId claim, and the agent keys its project-runner mode
|
|
245
|
-
# off the var being ABSENT (never an empty-string export).
|
|
246
|
-
CONVEYOR_TASK_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.taskId // empty' 2>/dev/null || true)
|
|
247
|
-
if [ -n "${CONVEYOR_TASK_ID}" ]; then
|
|
248
|
-
export CONVEYOR_TASK_ID
|
|
249
|
-
else
|
|
250
|
-
unset CONVEYOR_TASK_ID
|
|
251
|
-
fi
|
|
252
|
-
# projectId claim is present only on task-less project session JWTs.
|
|
253
|
-
CONVEYOR_PROJECT_ID_CLAIM=$(echo "${SESSION_CLAIMS}" | jq -r '.projectId // empty' 2>/dev/null || true)
|
|
254
|
-
if [ -n "${CONVEYOR_PROJECT_ID_CLAIM}" ]; then
|
|
255
|
-
export CONVEYOR_PROJECT_ID="${CONVEYOR_PROJECT_ID_CLAIM}"
|
|
256
|
-
fi
|
|
257
|
-
export CONVEYOR_SESSION_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.sessionId // empty')
|
|
258
|
-
export CONVEYOR_WORKSPACE_ID=$(echo "${SESSION_CLAIMS}" | jq -r '.workspaceId // empty')
|
|
259
|
-
SESSION_MODE=$(echo "${SESSION_CLAIMS}" | jq -r '.mode // empty')
|
|
260
|
-
unset SESSION_CLAIMS
|
|
261
|
-
if [ "${SESSION_MODE}" = "review" ]; then
|
|
262
|
-
# Review sessions run the PR-review runner. Keyed on the session MODE claim,
|
|
263
|
-
# not the role: review sessions are writers (they need push access to the PR
|
|
264
|
-
# branch), so a role check can't identify them — gating on role=reader here
|
|
265
|
-
# is what let review pods fall through to the default task runner and boot
|
|
266
|
-
# the parent's task agent in discovery mode. The agent CLI's RunnerMode is
|
|
267
|
-
# spelled "code-review" (cli.ts validates task|pm|code-review|adhoc|pack); the bare
|
|
268
|
-
# "review" is the agentMode/tag axis, NOT a runner mode — passing it here
|
|
269
|
-
# made the agent exit "Invalid CONVEYOR_MODE" and crash-loop every 10s.
|
|
270
|
-
export CONVEYOR_MODE="code-review"
|
|
271
|
-
elif [ "${SESSION_MODE}" = "pack" ]; then
|
|
272
|
-
# Parent-card orchestrator (task-bound — CONVEYOR_TASK_ID is set): the agent
|
|
273
|
-
# runs the autonomous pack loop (start ready children, merge child PRs,
|
|
274
|
-
# complete the parent) instead of building code. Same task-mode lifecycle,
|
|
275
|
-
# different prompt/tool surface — see conveyor-agent pack-runner-prompt.ts.
|
|
276
|
-
export CONVEYOR_MODE="pack"
|
|
277
|
-
elif [ "${SESSION_MODE}" = "adhoc" ]; then
|
|
278
|
-
# Task-less USER SCRATCH pod (Sessions view): the agent runs an interactive
|
|
279
|
-
# `claude` TUI relayed to the web terminal — no autonomous loop, no task. Must
|
|
280
|
-
# be checked BEFORE the project branch below: an adhoc session is also
|
|
281
|
-
# task-less with a projectId claim, but it must NOT boot the pm project runner.
|
|
282
|
-
export CONVEYOR_MODE="adhoc"
|
|
283
|
-
elif [ -z "${CONVEYOR_TASK_ID:-}" ] && [ -n "${CONVEYOR_PROJECT_ID_CLAIM}" ]; then
|
|
284
|
-
# Task-less project pod: the agent boots the project runner in pm mode
|
|
285
|
-
# (see conveyor-agent setup/project-identity.ts — pm is required).
|
|
286
|
-
export CONVEYOR_MODE="pm"
|
|
287
|
-
fi
|
|
288
|
-
|
|
289
|
-
# ── Export bundle env vars (secrets, OAuth tokens, project config) ──
|
|
290
|
-
ENV_KEYS=$(echo "${BOOTSTRAP_JSON}" | jq -r '.envVars // {} | keys[]' 2>/dev/null)
|
|
291
|
-
if [ -n "${ENV_KEYS}" ]; then
|
|
292
|
-
ENV_COUNT=0
|
|
293
|
-
while IFS= read -r key; do
|
|
294
|
-
# A bundle key that isn't a valid shell identifier would make `export`
|
|
295
|
-
# eval arbitrary content, and a handful of names would hijack the boot
|
|
296
|
-
# itself (PATH swaps every binary below; LD_PRELOAD injects code into
|
|
297
|
-
# them). Project env is user-supplied — validate, never trust.
|
|
298
|
-
if ! [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
299
|
-
echo "[boot] WARN: skipping invalid env key from bundle: '${key}'"
|
|
300
|
-
continue
|
|
301
|
-
fi
|
|
302
|
-
case "${key}" in
|
|
303
|
-
PATH|HOME|LD_PRELOAD|SHELL)
|
|
304
|
-
echo "[boot] WARN: skipping denylisted env key from bundle: '${key}'"
|
|
305
|
-
continue
|
|
306
|
-
;;
|
|
307
|
-
esac
|
|
308
|
-
value=$(echo "${BOOTSTRAP_JSON}" | jq -r --arg k "${key}" '.envVars[$k] // empty')
|
|
309
|
-
export "${key}=${value}"
|
|
310
|
-
ENV_COUNT=$((ENV_COUNT + 1))
|
|
311
|
-
done <<< "${ENV_KEYS}"
|
|
312
|
-
echo "[boot] Injected ${ENV_COUNT} env vars from bootstrap bundle"
|
|
313
|
-
fi
|
|
314
|
-
|
|
315
|
-
# ── Boot timing instrumentation (env-inject → agent-launch breakdown) ──
|
|
316
|
-
# A ~16s window between "Injected env vars" and "Linking Claude state" was
|
|
317
|
-
# root-caused (2026-07-16) to the unconditional root-owned-$HOME `chown -R` that
|
|
318
|
-
# used to run here — NOT GCS-FUSE (the mount is idle during the gap). That chown
|
|
319
|
-
# is now a conditional pre-bind reclaim (see reclaim_home_ownership). These marks
|
|
320
|
-
# stay as a regression probe: they attribute the gap to each sub-step —
|
|
321
|
-
# mount-readiness stat vs first metadata write vs symlink wiring — so if the
|
|
322
|
-
# total ever balloons again a canary boot shows WHERE. EPOCHREALTIME is
|
|
323
|
-
# bash-native (no per-mark subprocess); the `//[.,]/` strip tolerates
|
|
324
|
-
# comma-decimal locales. Grep the boot log for `[boot-timing]`. See
|
|
325
|
-
# docs/investigations/gcs-fuse-user-home-boot-cost-2026-07-15.md.
|
|
326
|
-
BOOT_MARK_T0_US="${EPOCHREALTIME//[.,]/}"
|
|
327
|
-
boot_mark() {
|
|
328
|
-
if [ -z "${EPOCHREALTIME:-}" ]; then echo "[boot-timing] $1"; return 0; fi
|
|
329
|
-
local now_us="${EPOCHREALTIME//[.,]/}"
|
|
330
|
-
local delta_us=$(( now_us - BOOT_MARK_T0_US ))
|
|
331
|
-
printf '[boot-timing] +%d.%03ds %s\n' "$(( delta_us / 1000000 ))" "$(( (delta_us / 1000) % 1000 ))" "$1"
|
|
332
|
-
}
|
|
333
|
-
boot_mark "env injected — entering user-home wiring"
|
|
334
|
-
|
|
335
|
-
export ANTHROPIC_API_KEY=$(echo "${BOOTSTRAP_JSON}" | jq -r '.anthropicKey // empty')
|
|
336
|
-
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gcpToken // empty')
|
|
337
|
-
unset BOOTSTRAP_JSON
|
|
338
|
-
# POD_BOOTSTRAP_TOKEN is deliberately NOT unset: it is the credential-refresh
|
|
339
|
-
# key. The agent re-polls GET /api/v3/pods/bootstrap with it to swap in a
|
|
340
|
-
# fresh githubToken/sessionJwt in place (see agent-connection refresh path).
|
|
341
|
-
|
|
342
|
-
CONVEYOR_USER_ID="${CONVEYOR_USER_ID:-}"
|
|
343
|
-
CONVEYOR_PROJECT_ID_FROM_BUNDLE="${CONVEYOR_PROJECT_ID:-${PROJECT_ID:-}}"
|
|
344
|
-
export CONVEYOR_USER_ID
|
|
345
|
-
|
|
346
|
-
# ── Seed Claude Code's first-run gates ──
|
|
347
|
-
# Pre-answer every interactive gate the CLI shows on a fresh (or partially
|
|
348
|
-
# initialized) config: the onboarding wizard (theme picker), the
|
|
349
|
-
# bypass-permissions warning, and the folder-trust dialog for the repo
|
|
350
|
-
# workspace. Any one of these parks a headless TUI forever — seen live
|
|
351
|
-
# 2026-07-02 as code reviews stuck at "Is this a project you trust?" until the
|
|
352
|
-
# server's cap burned the attempt. conveyor-agent also seeds the same keys before
|
|
353
|
-
# each spawn (ensureClaudeOnboarding), but this shell seed is version-independent
|
|
354
|
-
# (pods can run older baked agents) and lands before the CLI's first read — and
|
|
355
|
-
# it also covers an interactive `claude` opened over an SSH tunnel, which the
|
|
356
|
-
# agent seed never runs for. Merge via jq, never clobber: CLI-owned keys (cache
|
|
357
|
-
# keys, firstStartTime, other projects' trust entries) survive; a missing or
|
|
358
|
-
# corrupt file becomes a fresh seed — Claude Code treats a 0-byte/unparseable
|
|
359
|
-
# file as fatal ("Unexpected end of JSON input"), so replacing is a repair. Theme
|
|
360
|
-
# only seeds fresh configs (a user-picked theme is never overridden). Every step
|
|
361
|
-
# is rescued: config seeding must never kill the entrypoint under `set -eo pipefail`.
|
|
362
|
-
seed_claude_json() {
|
|
363
|
-
SEED_TARGET="$1"
|
|
364
|
-
SEED_GATES='{"hasCompletedOnboarding":true,"bypassPermissionsModeAccepted":true,"projects":{"/workspaces/repo":{"hasTrustDialogAccepted":true}}}'
|
|
365
|
-
# Fable is entitlement-gated in the CLI's /model picker via
|
|
366
|
-
# additionalModelOptionsCache, normally populated by a bootstrap fetch the
|
|
367
|
-
# pods' synthesized credentials don't satisfy — without this row the picker
|
|
368
|
-
# never offers Fable in a pod. Appended only when no fable-valued entry
|
|
369
|
-
# exists, so a CLI-written entitlement cache is never clobbered.
|
|
370
|
-
SEED_FABLE_OPTION='{"value":"claude-fable-5[1m]","label":"Fable","description":"Fable 5 - most capable for your hardest and longest-running tasks"}'
|
|
371
|
-
SEED_FABLE_FILTER='if ([.additionalModelOptionsCache[]? | .value? | strings | ascii_downcase | select(contains("fable"))] | length) > 0 then . else .additionalModelOptionsCache = ((.additionalModelOptionsCache // []) + [$fable]) end'
|
|
372
|
-
if [ -f "${SEED_TARGET}" ] && jq -e . "${SEED_TARGET}" >/dev/null 2>&1; then
|
|
373
|
-
if SEED_MERGED="$(jq -c --argjson gates "${SEED_GATES}" --argjson fable "${SEED_FABLE_OPTION}" ". * \$gates | ${SEED_FABLE_FILTER}" "${SEED_TARGET}" 2>/dev/null)" && [ -n "${SEED_MERGED}" ]; then
|
|
374
|
-
printf '%s' "${SEED_MERGED}" > "${SEED_TARGET}" 2>/dev/null || true
|
|
375
|
-
fi
|
|
376
|
-
else
|
|
377
|
-
printf '%s' "${SEED_GATES}" | jq -c --argjson fable "${SEED_FABLE_OPTION}" '. + {theme:"dark", additionalModelOptionsCache: [$fable]}' > "${SEED_TARGET}" 2>/dev/null || true
|
|
378
|
-
fi
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
# ── Wire per-user Claude state from the GCS FUSE mount ──
|
|
382
|
-
# When the pod has /mnt/conveyor-users mounted (GCS FUSE CSI), symlink
|
|
383
|
-
# ~/.claude, ~/.claude.json, and ~/.config/claude to the user's subdir so
|
|
384
|
-
# Claude state persists across all of the user's codespaces without any
|
|
385
|
-
# tar/upload/download round-trip. Uses projectId if present, else falls
|
|
386
|
-
# back to PROJECT_ID from the pod env.
|
|
387
|
-
USER_HOME_MOUNT="/mnt/conveyor-users"
|
|
388
|
-
USER_HOME_PROJECT_ID="${CONVEYOR_PROJECT_ID_FROM_BUNDLE:-${PROJECT_ID:-}}"
|
|
389
|
-
|
|
390
|
-
# Root-owned $HOME reclaim moved OFF this critical path into the pre-bind phase
|
|
391
|
-
# (reclaim_home_ownership, called after refresh_agent_version) and made
|
|
392
|
-
# conditional — the former unconditional `chown -R` here cost ~16s of copy-up on
|
|
393
|
-
# every boot and was the real owner of this "GCS-FUSE" gap. See
|
|
394
|
-
# docs/investigations/gcs-fuse-user-home-boot-cost-2026-07-15.md.
|
|
395
|
-
|
|
396
|
-
# The `[ -d "${USER_HOME_MOUNT}" ]` test below is the first stat against the FUSE
|
|
397
|
-
# mount — if the gcsfuse sidecar is still handshaking it blocks here, so the
|
|
398
|
-
# mark straddles the readiness probe.
|
|
399
|
-
boot_mark "probing user-home mount presence"
|
|
400
|
-
if [ -n "${CONVEYOR_USER_ID}" ] && [ -n "${USER_HOME_PROJECT_ID}" ] && [ -d "${USER_HOME_MOUNT}" ]; then
|
|
401
|
-
USER_HOME_ROOT="${USER_HOME_MOUNT}/users/${CONVEYOR_USER_ID}/${USER_HOME_PROJECT_ID}"
|
|
402
|
-
boot_mark "user-home mount ready — linking Claude state"
|
|
403
|
-
echo "[pool] Linking Claude state to ${USER_HOME_ROOT}"
|
|
404
|
-
mkdir -p "${USER_HOME_ROOT}/.claude" "${USER_HOME_ROOT}/.config/claude" 2>/dev/null || true
|
|
405
|
-
boot_mark "mkdir into mount done (first metadata write)"
|
|
406
|
-
|
|
407
|
-
# Replace any image defaults with live symlinks into the mount for the SHARED,
|
|
408
|
-
# append-mostly Claude state: transcripts/plans/memory under ~/.claude and
|
|
409
|
-
# ~/.config/claude. These persist across every pod the user runs, and --resume
|
|
410
|
-
# + session history depend on them (see claude-session-persistence.md).
|
|
411
|
-
#
|
|
412
|
-
# ~/.claude.json is deliberately NOT symlinked — it is POD-LOCAL (below). The
|
|
413
|
-
# Claude CLI rewrites that file wholesale at startup and during runs, so a
|
|
414
|
-
# burst of concurrent pods for the same user+project (seen live 2026-07-12: 7
|
|
415
|
-
# pods in 10 minutes) racing one shared file produces last-writer-wins lost
|
|
416
|
-
# updates — the true root cause behind the read-back mismatch that parked a
|
|
417
|
-
# TUI at a startup dialog, and the vector that let one poisoned
|
|
418
|
-
# customApiKeyResponses write brick every future pod. Nothing load-bearing
|
|
419
|
-
# lives only in ~/.claude.json: credentials are in ~/.claude/.credentials.json
|
|
420
|
-
# (pod-local via symlink, see below) and transcripts in ~/.claude/projects.
|
|
421
|
-
# The agent's ensureClaudeOnboarding + the seed below rebuild every first-run
|
|
422
|
-
# gate this file needs, fresh, on each boot.
|
|
423
|
-
rm -rf /home/conveyor/.claude /home/conveyor/.config/claude 2>/dev/null || true
|
|
424
|
-
mkdir -p /home/conveyor/.config 2>/dev/null || true
|
|
425
|
-
ln -sfn "${USER_HOME_ROOT}/.claude" /home/conveyor/.claude
|
|
426
|
-
ln -sfn "${USER_HOME_ROOT}/.config/claude" /home/conveyor/.config/claude
|
|
427
|
-
boot_mark "~/.claude + ~/.config/claude symlinks wired"
|
|
428
|
-
|
|
429
|
-
# Pod-local ~/.claude.json: clear any symlink/file a prior boot or the image
|
|
430
|
-
# baked in, then seed a fresh real file on the pod's OWN disk (local
|
|
431
|
-
# read-after-write is consistent, so no cross-pod race and no read-back skew).
|
|
432
|
-
rm -rf /home/conveyor/.claude.json 2>/dev/null || true
|
|
433
|
-
seed_claude_json /home/conveyor/.claude.json
|
|
434
|
-
|
|
435
|
-
# Pod-local ~/.claude/.credentials.json: the TUI and the usage probe
|
|
436
|
-
# authenticate from this file, while the server attributes usage samples to
|
|
437
|
-
# the key stamped on THIS pod's session. As a shared file (it lives inside
|
|
438
|
-
# the symlinked ~/.claude), concurrent pods booted under different keys
|
|
439
|
-
# clobbered it last-writer-wins, so every pod probed whichever ACCOUNT booted
|
|
440
|
-
# most recently — cross-wiring usage attribution between the user's keys
|
|
441
|
-
# (seen live 2026-07-14). Same cure as ~/.claude.json above, except the CLI
|
|
442
|
-
# derives this path from ~/.claude, so the shared dir keeps a SYMLINK to an
|
|
443
|
-
# absolute pod-local path — which resolves per-pod. Racing pods all write the
|
|
444
|
-
# identical symlink value, so the ln itself cannot lose data. Best-effort: on
|
|
445
|
-
# failure the file stays shared and the agent's sampler identity guard still
|
|
446
|
-
# blocks wrong-account attribution.
|
|
447
|
-
CRED_SHARED="${USER_HOME_ROOT}/.claude/.credentials.json"
|
|
448
|
-
CRED_POD_LOCAL="/home/conveyor/.claude-credentials.pod.json"
|
|
449
|
-
# Split-mode pods: the agent container WRITES credentials (harness auth) and
|
|
450
|
-
# claude in the workbench READS them through the shared-dir symlink — so the
|
|
451
|
-
# pod-local file must live on the shared emptyDir. Still pod-local, so the
|
|
452
|
-
# concurrent-pods clobber fix above is preserved.
|
|
453
|
-
if [ -n "${CONVEYOR_SHARED_DIR:-}" ]; then
|
|
454
|
-
CRED_POD_LOCAL="${CONVEYOR_SHARED_DIR}/claude-credentials.pod.json"
|
|
455
|
-
fi
|
|
456
|
-
if [ -f "${CRED_SHARED}" ] && [ ! -L "${CRED_SHARED}" ]; then
|
|
457
|
-
# Legacy shared regular file: carry its contents into this pod so auth
|
|
458
|
-
# survives the cutover; the agent re-synthesizes from its own token at
|
|
459
|
-
# spawn anyway.
|
|
460
|
-
cp "${CRED_SHARED}" "${CRED_POD_LOCAL}" 2>/dev/null || true
|
|
461
|
-
fi
|
|
462
|
-
ln -sfn "${CRED_POD_LOCAL}" "${CRED_SHARED}" 2>/dev/null || true
|
|
463
|
-
chmod 600 "${CRED_POD_LOCAL}" 2>/dev/null || true
|
|
464
|
-
boot_mark "~/.claude.json seeded + credentials repointed"
|
|
465
|
-
|
|
466
|
-
# OpenCode state (sessions + config) persists the same way as ~/.claude.
|
|
467
|
-
mkdir -p "${USER_HOME_ROOT}/.local/share/opencode" "${USER_HOME_ROOT}/.config/opencode" 2>/dev/null || true
|
|
468
|
-
rm -rf /home/conveyor/.local/share/opencode /home/conveyor/.config/opencode 2>/dev/null || true
|
|
469
|
-
mkdir -p /home/conveyor/.local/share 2>/dev/null || true
|
|
470
|
-
ln -sfn "${USER_HOME_ROOT}/.local/share/opencode" /home/conveyor/.local/share/opencode
|
|
471
|
-
ln -sfn "${USER_HOME_ROOT}/.config/opencode" /home/conveyor/.config/opencode
|
|
472
|
-
boot_mark "opencode state linked — user-home wiring complete"
|
|
473
|
-
else
|
|
474
|
-
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))"
|
|
475
|
-
# No persistent home — the CLI reads the pod-local config; seed it there so
|
|
476
|
-
# non-FUSE pods get the same first-run gate suppression as the mounted path.
|
|
477
|
-
seed_claude_json /home/conveyor/.claude.json
|
|
478
|
-
# Split-mode pods without a persistent user-home: ~/.claude must still cross
|
|
479
|
-
# the container boundary (claude writes transcripts in the workbench; the
|
|
480
|
-
# agent's tailer reads them), so it lives on the shared emptyDir.
|
|
481
|
-
if [ -n "${CONVEYOR_SHARED_DIR:-}" ]; then
|
|
482
|
-
mkdir -p "${CONVEYOR_SHARED_DIR}/claude-home/.claude" 2>/dev/null || true
|
|
483
|
-
rm -rf /home/conveyor/.claude 2>/dev/null || true
|
|
484
|
-
ln -sfn "${CONVEYOR_SHARED_DIR}/claude-home/.claude" /home/conveyor/.claude
|
|
485
|
-
fi
|
|
486
|
-
boot_mark "no user-home mount — pod-local seed only"
|
|
487
|
-
fi
|
|
488
|
-
|
|
489
|
-
# ── Wire published graphify bundles from the shared user-home mount ──
|
|
490
|
-
# A locally published graph lives in the same GCS-FUSE user-home bucket as
|
|
491
|
-
# Claude state, under users/_shared/graphify/<repo>/latest. Export the path
|
|
492
|
-
# contract before the agent starts, then bind the files into graphify-out after
|
|
493
|
-
# git has prepared the workspace but before the ready marker is released.
|
|
494
|
-
configure_graphify_env() {
|
|
495
|
-
if [ "${CONVEYOR_GRAPHIFY_DISABLE:-}" = "1" ]; then
|
|
496
|
-
return 0
|
|
497
|
-
fi
|
|
498
|
-
|
|
499
|
-
local slug="${CONVEYOR_GRAPHIFY_SLUG:-${REPO_NAME:-}}"
|
|
500
|
-
if [ -z "${slug}" ]; then
|
|
501
|
-
return 0
|
|
502
|
-
fi
|
|
503
|
-
|
|
504
|
-
local primary_root="${USER_HOME_MOUNT}/users/_shared/graphify"
|
|
505
|
-
local legacy_root="${USER_HOME_MOUNT}/_shared/graphify"
|
|
506
|
-
local shared_root="${CONVEYOR_GRAPHIFY_SHARED_ROOT:-}"
|
|
507
|
-
if [ -z "${shared_root}" ]; then
|
|
508
|
-
if [ -d "${primary_root}" ] || [ ! -d "${legacy_root}" ]; then
|
|
509
|
-
shared_root="${primary_root}"
|
|
510
|
-
else
|
|
511
|
-
shared_root="${legacy_root}"
|
|
512
|
-
fi
|
|
513
|
-
fi
|
|
514
|
-
|
|
515
|
-
export CONVEYOR_GRAPHIFY_SLUG="${slug}"
|
|
516
|
-
export CONVEYOR_GRAPHIFY_SHARED_ROOT="${shared_root}"
|
|
517
|
-
export CONVEYOR_GRAPHIFY_DIR="${CONVEYOR_GRAPHIFY_DIR:-${shared_root}/${slug}/latest}"
|
|
518
|
-
export CONVEYOR_GRAPHIFY_GRAPH="${CONVEYOR_GRAPHIFY_GRAPH:-${CONVEYOR_GRAPHIFY_DIR}/graph.json}"
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
bind_graphify_bundle() {
|
|
522
|
-
if [ "${CONVEYOR_GRAPHIFY_DISABLE:-}" = "1" ]; then
|
|
523
|
-
echo "[pool] Graphify bind disabled."
|
|
524
|
-
return 0
|
|
525
|
-
fi
|
|
526
|
-
|
|
527
|
-
if [ -z "${CONVEYOR_GRAPHIFY_SLUG:-}" ]; then
|
|
528
|
-
return 0
|
|
529
|
-
fi
|
|
530
|
-
|
|
531
|
-
local workspace="${CONVEYOR_WORKSPACE:-/workspaces/repo}"
|
|
532
|
-
local source_dir="${CONVEYOR_GRAPHIFY_DIR:-}"
|
|
533
|
-
local graph_file="${CONVEYOR_GRAPHIFY_GRAPH:-}"
|
|
534
|
-
|
|
535
|
-
if [ -z "${source_dir}" ] || [ -z "${graph_file}" ]; then
|
|
536
|
-
return 0
|
|
537
|
-
fi
|
|
538
|
-
|
|
539
|
-
if [ ! -f "${graph_file}" ]; then
|
|
540
|
-
echo "[pool] Graphify bundle not found for '${CONVEYOR_GRAPHIFY_SLUG}' at ${source_dir}"
|
|
541
|
-
return 0
|
|
542
|
-
fi
|
|
543
|
-
|
|
544
|
-
local target_dir="${workspace}/graphify-out"
|
|
545
|
-
if ! mkdir -p "${target_dir}" 2>/dev/null; then
|
|
546
|
-
echo "[pool] WARN: unable to create graphify-out at ${target_dir}"
|
|
547
|
-
return 0
|
|
548
|
-
fi
|
|
549
|
-
|
|
550
|
-
local rel
|
|
551
|
-
for rel in graph.json GRAPH_REPORT.md manifest.json .graphify_analysis.json .graphify_labels.json publish-manifest.json cost.json; do
|
|
552
|
-
if [ ! -e "${source_dir}/${rel}" ]; then
|
|
553
|
-
continue
|
|
554
|
-
fi
|
|
555
|
-
if [ -e "${target_dir}/${rel}" ] || [ -L "${target_dir}/${rel}" ]; then
|
|
556
|
-
continue
|
|
557
|
-
fi
|
|
558
|
-
ln -s "${source_dir}/${rel}" "${target_dir}/${rel}" 2>/dev/null || true
|
|
559
|
-
done
|
|
560
|
-
|
|
561
|
-
echo "[pool] Bound graphify bundle '${CONVEYOR_GRAPHIFY_SLUG}' into ${target_dir}"
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
# ── Link shared Grimoire skills into the repo's project skill dir ──
|
|
565
|
-
# The prebake links grimoire skills into the BAKED image's ~/.claude/skills,
|
|
566
|
-
# but the user-home wiring above replaces /home/conveyor/.claude with the
|
|
567
|
-
# per-user GCS mount on every mounted pod boot — wiping those links before any
|
|
568
|
-
# agent runs (observed fleet-wide 2026-07-10: pods saw only repo-tracked
|
|
569
|
-
# skills, none of the shared rc-* set). Link repo-locally instead: Claude
|
|
570
|
-
# loads project skills from <workspace>/.claude/skills regardless of where
|
|
571
|
-
# $HOME points, the links are pod-local (no writes to the shared mount, no
|
|
572
|
-
# cross-pod races), and .gitignore covers them so `git status` stays clean.
|
|
573
|
-
# Runs inside mark_git_ready — after checkout is final, before the agent may
|
|
574
|
-
# spawn Claude — alongside the graphify bundle bind. Both helpers are
|
|
575
|
-
# best-effort: a grimoire failure must never block the git-ready gate.
|
|
576
|
-
ensure_grimoire_submodule() {
|
|
577
|
-
local workspace="${CONVEYOR_WORKSPACE:-/workspaces/repo}"
|
|
578
|
-
[ -f "${workspace}/.gitmodules" ] || return 0
|
|
579
|
-
git -C "${workspace}" config --file .gitmodules --get-regexp 'submodule\..*\.path' 2>/dev/null \
|
|
580
|
-
| grep -q '\.claude/grimoire$' || return 0
|
|
581
|
-
# Already materialized (pod-image bake ran conveyor-prebake successfully).
|
|
582
|
-
[ -d "${workspace}/.claude/grimoire/skills" ] && return 0
|
|
583
|
-
if [ -z "${CONVEYOR_GITHUB_TOKEN:-}" ]; then
|
|
584
|
-
echo "[pool] WARN: grimoire submodule absent and no token to fetch it"
|
|
585
|
-
return 0
|
|
586
|
-
fi
|
|
587
|
-
# insteadOf injects the installation token for the submodule's https URL the
|
|
588
|
-
# same way sync_task_branch_to_repo authenticates the main repo remote.
|
|
589
|
-
if _grim_out=$(git -C "${workspace}" \
|
|
590
|
-
-c url."https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/".insteadOf="https://github.com/" \
|
|
591
|
-
submodule update --init .claude/grimoire 2>&1); then
|
|
592
|
-
echo "[pool] Initialized grimoire submodule"
|
|
593
|
-
else
|
|
594
|
-
echo "[pool] WARN: grimoire submodule init failed (baked pods may lack rc-* skills): ${_grim_out}"
|
|
595
|
-
fi
|
|
596
|
-
return 0
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
link_grimoire_skills() {
|
|
600
|
-
local workspace="${CONVEYOR_WORKSPACE:-/workspaces/repo}"
|
|
601
|
-
local source_dir="${workspace}/.claude/grimoire/skills"
|
|
602
|
-
local target_dir="${workspace}/.claude/skills"
|
|
603
|
-
[ -d "${source_dir}" ] || return 0
|
|
604
|
-
if ! mkdir -p "${target_dir}" 2>/dev/null; then
|
|
605
|
-
echo "[pool] WARN: unable to create ${target_dir}; skipping grimoire skill links"
|
|
606
|
-
return 0
|
|
607
|
-
fi
|
|
608
|
-
local skill_dir name linked=0
|
|
609
|
-
for skill_dir in "${source_dir}"/*/; do
|
|
610
|
-
[ -d "${skill_dir}" ] || continue
|
|
611
|
-
name="$(basename "${skill_dir}")"
|
|
612
|
-
# Never clobber a real (repo-tracked) skill dir with a link.
|
|
613
|
-
if [ -e "${target_dir}/${name}" ] && [ ! -L "${target_dir}/${name}" ]; then
|
|
614
|
-
echo "[pool] WARN: ${target_dir}/${name} exists and is not a symlink; skipping"
|
|
615
|
-
continue
|
|
616
|
-
fi
|
|
617
|
-
ln -sfn "../grimoire/skills/${name}" "${target_dir}/${name}" 2>/dev/null && linked=$((linked + 1))
|
|
618
|
-
done
|
|
619
|
-
echo "[pool] Linked ${linked} grimoire skills into ${target_dir}"
|
|
620
|
-
return 0
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
mark_git_ready() {
|
|
624
|
-
bind_graphify_bundle
|
|
625
|
-
ensure_grimoire_submodule
|
|
626
|
-
link_grimoire_skills
|
|
627
|
-
: > "$GIT_READY_MARKER"
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
configure_graphify_env
|
|
631
|
-
|
|
632
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
633
|
-
# WORKSPACE GIT — moved OFF the pre-launch critical path (Claudespace v3).
|
|
634
|
-
#
|
|
635
|
-
# The agent is launched IMMEDIATELY (below) so the card lights up and setup
|
|
636
|
-
# output streams while git runs in the BACKGROUND. `prepare_workspace_git`
|
|
637
|
-
# does the full fetch/checkout/merge (task repo) then clones reference repos,
|
|
638
|
-
# and signals completion by writing exactly ONE marker file:
|
|
639
|
-
#
|
|
640
|
-
# GIT_READY_MARKER — task repo is up to date; the agent may spawn Claude.
|
|
641
|
-
# GIT_FAILED_MARKER — git preparation errored; the agent surfaces the error
|
|
642
|
-
# and shuts down WITHOUT operating on a broken/stale repo.
|
|
643
|
-
#
|
|
644
|
-
# This function runs backgrounded (`prepare_workspace_git &`). It therefore
|
|
645
|
-
# must NEVER `exit` (that only kills the subshell, leaving the agent to wait
|
|
646
|
-
# on a marker that never arrives) — every error path does
|
|
647
|
-
# `echo >&2; printf ... > "$GIT_FAILED_MARKER"; return 1` instead. The
|
|
648
|
-
# fail-loud-on-stale-image philosophy is preserved: a failure writes the failed
|
|
649
|
-
# marker (was: exit 1), which the agent treats as fatal.
|
|
650
|
-
#
|
|
651
|
-
# `set -e` interaction: fallible git commands keep the existing
|
|
652
|
-
# `if ! _out=$(...); then` guard so a non-zero rc reaches our marker write
|
|
653
|
-
# rather than aborting the subshell before it. Every exit path writes exactly
|
|
654
|
-
# one marker.
|
|
655
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
656
|
-
GIT_READY_MARKER="/workspaces/.conveyor-git-ready"
|
|
657
|
-
GIT_FAILED_MARKER="/workspaces/.conveyor-git-failed"
|
|
658
|
-
# Split-mode pods: the markers live on the shared emptyDir so the workbench's
|
|
659
|
-
# git prep is visible to the agent container (which never sees /workspaces).
|
|
660
|
-
if [ -n "${CONVEYOR_SHARED_DIR:-}" ]; then
|
|
661
|
-
GIT_READY_MARKER="${CONVEYOR_SHARED_DIR}/git-ready"
|
|
662
|
-
GIT_FAILED_MARKER="${CONVEYOR_SHARED_DIR}/git-failed"
|
|
663
|
-
fi
|
|
664
|
-
|
|
665
|
-
reset_tracked_repo_changes_before_assignment_checkout() {
|
|
666
|
-
# The baked/pooled repo is not user-owned until this git gate succeeds. Reset
|
|
667
|
-
# stale tracked image-generated dirt so assignment checkout can move to the
|
|
668
|
-
# requested branch/ref instead of being blocked by files like dependency
|
|
669
|
-
# stamps. Do not `git clean`: untracked prebake artifacts may be intentional.
|
|
670
|
-
# The assignment checkouts additionally pass -f: an UNTRACKED bake artifact
|
|
671
|
-
# can collide with a path the TARGET ref tracks (e.g. a dependency stamp
|
|
672
|
-
# committed on an older PR branch), which this reset cannot clear — checkout
|
|
673
|
-
# then refuses with "untracked working tree files would be overwritten".
|
|
674
|
-
# Forcing is safe here for the same not-user-owned reason.
|
|
675
|
-
if ! _reset_out=$(git -C repo reset --hard HEAD 2>&1); then
|
|
676
|
-
echo "[pool] ERROR: failed to clean tracked repo changes before checkout: ${_reset_out}" >&2
|
|
677
|
-
printf '%s' "pre-checkout reset failed" > "$GIT_FAILED_MARKER"
|
|
678
|
-
return 1
|
|
679
|
-
fi
|
|
680
|
-
return 0
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
# Shared by both the pod-image branch and the repo-present-non-pod-image branch
|
|
684
|
-
# of prepare_workspace_git: refresh the remote token and warm origin/<base>.
|
|
685
|
-
# The agent (ensureOnTaskBranch, Tasks 4-5) now owns the task-branch
|
|
686
|
-
# fetch/checkout/create-from-base and the merge of base into the task branch —
|
|
687
|
-
# this function no longer touches BRANCH or CHECKOUT_REF at all. On success the
|
|
688
|
-
# repo is left on a clean tree with an up-to-date origin/<base> remote-tracking
|
|
689
|
-
# ref. On any failure it writes GIT_FAILED_MARKER and returns 1 — callers must
|
|
690
|
-
# not fall through to `: > "$GIT_READY_MARKER"` in that case.
|
|
691
|
-
sync_task_branch_to_repo() {
|
|
692
|
-
# Guarded so a set-url failure writes the failed marker instead of aborting
|
|
693
|
-
# the backgrounded subshell (which would leave the agent waiting forever).
|
|
694
|
-
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
|
|
695
|
-
echo "[pool] ERROR: remote set-url failed" >&2
|
|
696
|
-
printf '%s' "remote set-url failed" > "$GIT_FAILED_MARKER"
|
|
697
|
-
return 1
|
|
698
|
-
fi
|
|
699
|
-
# Warm origin/<base> so the agent's checkout/fetch is a fast-forward.
|
|
700
|
-
if ! _fetch_out=$(git -C repo fetch origin "+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>&1); then
|
|
701
|
-
echo "[pool] WARN: fetch origin/${BASE_BRANCH} failed: ${_fetch_out}" >&2
|
|
702
|
-
fi
|
|
703
|
-
# Leave the tree clean; the agent (ensureOnTaskBranch) does the authoritative
|
|
704
|
-
# checkout + conveyor-wip restore. No task-branch checkout, no dev merge here.
|
|
705
|
-
reset_tracked_repo_changes_before_assignment_checkout || return 1
|
|
706
|
-
echo "[pool] Repo remote ready; agent will checkout ${BRANCH}"
|
|
707
|
-
return 0
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
prepare_workspace_git() {
|
|
711
|
-
# Update remote URL with fresh token, or clone if pre-clone failed
|
|
712
|
-
cd /workspaces
|
|
713
|
-
if [ "${CONVEYOR_POD_IMAGE}" = "1" ] && [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
|
|
714
|
-
# Pod image: repo already exists (and pre-warm already fetched origin/dev, so
|
|
715
|
-
# the fetch below is a near-instant fast-forward). Just refresh the remote with
|
|
716
|
-
# the assignment's fresh token and bring the task branch up to date.
|
|
717
|
-
# IMPORTANT: Do NOT silently fall through to the image snapshot when the
|
|
718
|
-
# requested branch can't be fetched/checked out. A stale image repo has
|
|
719
|
-
# bitten us before (old load-env.sh, old scripts, wrong deps) and the
|
|
720
|
-
# symptoms are very hard to diagnose from pod logs. Fail loud instead —
|
|
721
|
-
# now via the failed marker rather than exit 1.
|
|
722
|
-
echo "[pool] Pod image — updating repo to latest (branch=${BRANCH})..."
|
|
723
|
-
if ! sync_task_branch_to_repo; then
|
|
724
|
-
return 1
|
|
725
|
-
fi
|
|
726
|
-
# Task repo is ready HERE — release the gate. The agent (already launched)
|
|
727
|
-
# is polling for this marker before it spawns Claude / runs setup. Reference
|
|
728
|
-
# repos are cloned AFTER this so they never block Claude.
|
|
729
|
-
mark_git_ready
|
|
730
|
-
elif [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
|
|
731
|
-
# Non-image but repo already present (e.g. a baked image running without
|
|
732
|
-
# CONVEYOR_POD_IMAGE set). Run the exact same remote-refresh + base-warm
|
|
733
|
-
# flow as the pod-image branch so this path ends up in the same
|
|
734
|
-
# clean-tree, remote-ready state; the agent (ensureOnTaskBranch) does the
|
|
735
|
-
# actual task-branch checkout regardless of which branch got it here.
|
|
736
|
-
echo "[pool] Repo present (non-pod-image) — updating repo to latest (branch=${BRANCH})..."
|
|
737
|
-
if ! sync_task_branch_to_repo; then
|
|
738
|
-
return 1
|
|
739
|
-
fi
|
|
740
|
-
mark_git_ready
|
|
741
|
-
elif [ -n "${CONVEYOR_GITHUB_TOKEN}" ] && [ -n "${REPO_OWNER}" ] && [ -n "${REPO_NAME}" ] && [ -n "${BRANCH}" ]; then
|
|
742
|
-
echo "[pool] Cloning repo post-assignment (pre-clone was missing)..."
|
|
743
|
-
# Guard each clone/fetch/checkout: under `set -e` a bare failing clone would
|
|
744
|
-
# abort the subshell before we can write the failed marker.
|
|
745
|
-
if [ -n "${CHECKOUT_REF}" ]; then
|
|
746
|
-
if ! _clone_out=$(git clone --depth 1 --single-branch --branch "${BASE_BRANCH}" \
|
|
747
|
-
"https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
|
|
748
|
-
repo 2>&1); then
|
|
749
|
-
echo "[pool] ERROR: post-assignment clone failed: ${_clone_out}" >&2
|
|
750
|
-
printf '%s' "post-assignment clone failed" > "$GIT_FAILED_MARKER"
|
|
751
|
-
return 1
|
|
752
|
-
fi
|
|
753
|
-
if ! _fetch_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
|
|
754
|
-
echo "[pool] ERROR: post-assignment fetch of ${CHECKOUT_REF} failed: ${_fetch_out}" >&2
|
|
755
|
-
printf '%s' "post-assignment fetch of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
756
|
-
return 1
|
|
757
|
-
fi
|
|
758
|
-
if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
|
|
759
|
-
echo "[pool] ERROR: post-assignment checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
|
|
760
|
-
printf '%s' "post-assignment checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
761
|
-
return 1
|
|
762
|
-
fi
|
|
763
|
-
else
|
|
764
|
-
# Clone the BASE branch — it always exists, unlike a brand-new task
|
|
765
|
-
# branch. A naive `clone --branch <task>` dies with "Remote branch not
|
|
766
|
-
# found" when the task branch has never been pushed to origin, which
|
|
767
|
-
# crash-loops the pod on every fresh task. sync_task_branch_to_repo below
|
|
768
|
-
# only refreshes the remote and warms origin/<base> from this clone; the
|
|
769
|
-
# agent (ensureOnTaskBranch) resolves/creates ${BRANCH} from it. Full
|
|
770
|
-
# depth (no --depth 1) so the agent's base-merge never hits "refusing to
|
|
771
|
-
# merge unrelated histories".
|
|
772
|
-
if ! _clone_out=$(git clone --single-branch --branch "${BASE_BRANCH}" \
|
|
773
|
-
"https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
|
|
774
|
-
repo 2>&1); then
|
|
775
|
-
echo "[pool] ERROR: post-assignment clone of base '${BASE_BRANCH}' failed: ${_clone_out}" >&2
|
|
776
|
-
printf '%s' "post-assignment clone failed" > "$GIT_FAILED_MARKER"
|
|
777
|
-
return 1
|
|
778
|
-
fi
|
|
779
|
-
if ! sync_task_branch_to_repo; then
|
|
780
|
-
return 1
|
|
781
|
-
fi
|
|
782
|
-
fi
|
|
783
|
-
mark_git_ready
|
|
784
|
-
else
|
|
785
|
-
# No git plan (e.g. task-less pod, or no token/repo). Nothing to prepare —
|
|
786
|
-
# the repo dir already exists (created before launch) and the agent's own
|
|
787
|
-
# git-sync fallback (guarded by CONVEYOR_GIT_READY) never runs on this pod
|
|
788
|
-
# path anyway. Signal ready so the agent doesn't wait out the timeout.
|
|
789
|
-
echo "[pool] No git plan to prepare — marking git ready."
|
|
790
|
-
mark_git_ready
|
|
791
|
-
fi
|
|
792
|
-
|
|
793
|
-
# ── Clone reference repos into /workspaces/references (best-effort) ──
|
|
794
|
-
# Populated from REFERENCE_REPOS_JSON (injected via envVars by
|
|
795
|
-
# injectReferenceRepos). Each repo is shallow-cloned as read-only context for
|
|
796
|
-
# the agent. Failures are non-fatal — the task must still proceed even if a
|
|
797
|
-
# reference project's GitHub App is uninstalled or the token mint failed.
|
|
798
|
-
# This runs AFTER the ready marker: references are supplementary context and
|
|
799
|
-
# must NOT block Claude from spawning.
|
|
800
|
-
if [ -n "${REFERENCE_REPOS_JSON:-}" ]; then
|
|
801
|
-
mkdir -p /workspaces/references
|
|
802
|
-
echo "${REFERENCE_REPOS_JSON}" | jq -c '.[]' 2>/dev/null | while IFS= read -r ref; do
|
|
803
|
-
REF_SLUG=$(echo "${ref}" | jq -r '.slug')
|
|
804
|
-
REF_OWNER=$(echo "${ref}" | jq -r '.owner')
|
|
805
|
-
REF_NAME=$(echo "${ref}" | jq -r '.name')
|
|
806
|
-
REF_BRANCH=$(echo "${ref}" | jq -r '.branch // "main"')
|
|
807
|
-
REF_TOKEN=$(echo "${ref}" | jq -r '.token // empty')
|
|
808
|
-
if [ -z "${REF_TOKEN}" ] || [ -z "${REF_SLUG}" ] || [ -z "${REF_OWNER}" ] || [ -z "${REF_NAME}" ]; then
|
|
809
|
-
continue
|
|
810
|
-
fi
|
|
811
|
-
if [ -d "/workspaces/references/${REF_SLUG}/.git" ]; then
|
|
812
|
-
continue
|
|
813
|
-
fi
|
|
814
|
-
if git clone --depth 1 --single-branch --branch "${REF_BRANCH}" \
|
|
815
|
-
"https://x-access-token:${REF_TOKEN}@github.com/${REF_OWNER}/${REF_NAME}.git" \
|
|
816
|
-
"/workspaces/references/${REF_SLUG}" 2>/dev/null; then
|
|
817
|
-
# Strip the token from the cloned remote so it never surfaces via
|
|
818
|
-
# `git remote -v` when the agent inspects the reference repo.
|
|
819
|
-
git -C "/workspaces/references/${REF_SLUG}" remote set-url origin \
|
|
820
|
-
"https://github.com/${REF_OWNER}/${REF_NAME}.git" 2>/dev/null || true
|
|
821
|
-
echo "[pool] cloned reference ${REF_SLUG} (${REF_OWNER}/${REF_NAME}@${REF_BRANCH})"
|
|
822
|
-
else
|
|
823
|
-
echo "[pool] WARN: reference clone failed: ${REF_SLUG}"
|
|
824
|
-
fi
|
|
825
|
-
done
|
|
826
|
-
unset REFERENCE_REPOS_JSON
|
|
827
|
-
fi
|
|
828
|
-
return 0
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
# Preview traffic is now proxied directly via k8s API pod proxy —
|
|
832
|
-
# no tunnel client needed. The API routes subdomain requests through
|
|
833
|
-
# the k8s API to reach the pod's ports directly.
|
|
834
|
-
|
|
835
|
-
# ── Clear stale markers, then background git and launch the agent early ──
|
|
836
|
-
# A leftover ready marker from a PRIOR pod on the baked image would be
|
|
837
|
-
# catastrophic: the agent would spawn Claude before THIS pod's git runs. Clear
|
|
838
|
-
# both before starting the background prep.
|
|
839
|
-
#
|
|
840
|
-
# Split-mode agent container: the WORKBENCH owns the repo and the git prep and
|
|
841
|
-
# writes the shared markers; this container only reads them. It must not clear
|
|
842
|
-
# them either — the workbench may have already written ready.
|
|
843
|
-
if [ "${CONTAINER_ROLE}" != "agent" ]; then
|
|
844
|
-
rm -f "$GIT_READY_MARKER" "$GIT_FAILED_MARKER"
|
|
845
|
-
# Ensure the repo dir exists for launch on BOTH paths (the background clone
|
|
846
|
-
# populates an empty dir on the non-image path; the pod-image path already has
|
|
847
|
-
# repo/.git).
|
|
848
|
-
mkdir -p /workspaces/repo
|
|
849
|
-
|
|
850
|
-
# Run the full git prep in the background so the agent can connect (card lights
|
|
851
|
-
# up) and stream setup output while git finishes.
|
|
852
|
-
prepare_workspace_git &
|
|
853
|
-
else
|
|
854
|
-
echo "[pool] agent role — skipping git prep (workbench owns the workspace)"
|
|
855
|
-
fi
|
|
856
|
-
|
|
857
|
-
# Signal to the agent that bash owns the git block (skip the agent's OWN
|
|
858
|
-
# git-sync fallback) and that it must await the ready marker before spawning
|
|
859
|
-
# Claude / running setup.
|
|
860
|
-
export CONVEYOR_GIT_READY=1
|
|
861
|
-
export CONVEYOR_GIT_READY_MARKER="$GIT_READY_MARKER"
|
|
862
|
-
export CONVEYOR_GIT_FAILED_MARKER="$GIT_FAILED_MARKER"
|
|
863
|
-
# Target the repo regardless of when the background clone lands.
|
|
864
|
-
export CONVEYOR_WORKSPACE=/workspaces/repo
|
|
865
|
-
|
|
866
|
-
# ── Workbench container (split-mode): exec the launcher daemon as PID 1 ──
|
|
867
|
-
# No crash loop here — the workbench is a native sidecar (restartPolicy
|
|
868
|
-
# Always); the kubelet owns restarts. Git prep above ran in THIS container
|
|
869
|
-
# (the repo lives here); the daemon serves the agent container's exec/pty/
|
|
870
|
-
# snapshot/file operations over loopback (token-authed, see workbench/server).
|
|
871
|
-
if [ "${CONTAINER_ROLE}" = "workbench" ]; then
|
|
872
|
-
# Bounded git-prep retry, backgrounded so the daemon starts immediately
|
|
873
|
-
# (the startupProbe gates pod readiness on the daemon listening). The repo
|
|
874
|
-
# lives HERE, so the retry must run here — the agent container's launch-loop
|
|
875
|
-
# retry is role-guarded off (found live 2026-07-16: an unguarded agent-side
|
|
876
|
-
# retry cloned the repo into the agent's own dead overlay).
|
|
877
|
-
(
|
|
878
|
-
_wb_git_attempts=0
|
|
879
|
-
while true; do
|
|
880
|
-
sleep 10
|
|
881
|
-
[ -f "$GIT_READY_MARKER" ] && break
|
|
882
|
-
if [ -f "$GIT_FAILED_MARKER" ]; then
|
|
883
|
-
if [ "$_wb_git_attempts" -ge 3 ]; then
|
|
884
|
-
echo "[pool] workbench git prep failed 3 times — giving up (agent surfaces the failure)."
|
|
885
|
-
break
|
|
886
|
-
fi
|
|
887
|
-
_wb_git_attempts=$((_wb_git_attempts + 1))
|
|
888
|
-
echo "[pool] workbench git prep failed — retrying (attempt ${_wb_git_attempts}/3)..."
|
|
889
|
-
rm -f "$GIT_READY_MARKER" "$GIT_FAILED_MARKER"
|
|
890
|
-
prepare_workspace_git
|
|
891
|
-
fi
|
|
892
|
-
done
|
|
893
|
-
) &
|
|
894
|
-
cd /workspaces/repo
|
|
895
|
-
boot_mark "launching workbench daemon (env-inject → daemon-launch total)"
|
|
896
|
-
echo "[pool] Launching workbench launcher daemon..."
|
|
897
|
-
export CONVEYOR_MODE=workbench
|
|
898
|
-
exec conveyor-agent
|
|
899
|
-
fi
|
|
900
|
-
|
|
901
|
-
# Launch agent — exit-code-aware restart loop.
|
|
902
|
-
# Exit 0 = clean shutdown (idle timeout, task complete) — pod dies.
|
|
903
|
-
# Non-zero = crash — retry after a brief pause.
|
|
904
|
-
#
|
|
905
|
-
# Split-mode agent container: /workspaces/repo in THIS container is a stale
|
|
906
|
-
# baked overlay, never the live tree — cwd must not resolve into it.
|
|
907
|
-
if [ "${CONTAINER_ROLE}" = "agent" ]; then
|
|
908
|
-
cd /home/conveyor
|
|
909
|
-
else
|
|
910
|
-
cd /workspaces/repo
|
|
911
|
-
fi
|
|
912
|
-
boot_mark "launching agent (env-inject → agent-launch total)"
|
|
913
|
-
echo "[pool] Launching agent..."
|
|
914
|
-
set +e
|
|
915
|
-
# Belt-and-braces alongside the pod's preStop hook (which pkills the agent
|
|
916
|
-
# directly because bash as PID 1 does not forward signals): if a SIGTERM does
|
|
917
|
-
# reach this shell (manual kill, future spec changes), forward it to the agent
|
|
918
|
-
# so flushGitOnShutdown still runs, then exit cleanly within the grace period.
|
|
919
|
-
trap 'echo "[pool] SIGTERM received, forwarding to agent..."; pkill -TERM -f conveyor-agent; wait; exit 0' TERM
|
|
920
|
-
# Bounded retry for a failed git prep. Without this, a transient git-prep
|
|
921
|
-
# failure writes GIT_FAILED_MARKER once and the agent's git gate then exits
|
|
922
|
-
# nonzero on every single relaunch forever (the marker never clears itself),
|
|
923
|
-
# crashlooping the pod every ~10s. Retry git prep itself, up to a small cap,
|
|
924
|
-
# before each relaunch; once the cap is exhausted stop hammering and fall
|
|
925
|
-
# back to the existing behavior (agent surfaces the failure) with a wider
|
|
926
|
-
# sleep so the pod idles instead of spinning.
|
|
927
|
-
GIT_PREP_MAX_RETRIES=3
|
|
928
|
-
_git_prep_attempts=0
|
|
929
|
-
# Bounded agent-crash supervision. An agent that keeps dying is a pod that keeps
|
|
930
|
-
# burning; cap the restarts, report every attempt to the API (the report posts to
|
|
931
|
-
# the task's activity log, which is also what keeps the pod alive through
|
|
932
|
-
# recovery), and exit for good once the cap is hit.
|
|
933
|
-
AGENT_CRASH_ATTEMPTS=0
|
|
934
|
-
AGENT_CRASH_MAX=3
|
|
935
|
-
|
|
936
|
-
report_agent_crash() {
|
|
937
|
-
# $1 = exit code, $2 = attempt, $3 = final (true/false). Best-effort.
|
|
938
|
-
curl -s -m 10 -X POST \
|
|
939
|
-
-H "Authorization: Bearer ${POD_BOOTSTRAP_TOKEN}" \
|
|
940
|
-
-H "Content-Type: application/json" \
|
|
941
|
-
-d "{\"exitCode\":${1},\"attempt\":${2},\"final\":${3}}" \
|
|
942
|
-
"${CONVEYOR_API_URL}/api/v3/pods/agent-crash" >/dev/null 2>&1 || true
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
while true; do
|
|
946
|
-
# Split-mode agent container: the workbench owns git prep AND its retry —
|
|
947
|
-
# re-running prep here would clone into this container's dead overlay.
|
|
948
|
-
if [ -f "$GIT_FAILED_MARKER" ] && [ "${CONTAINER_ROLE}" != "agent" ]; then
|
|
949
|
-
if [ "$_git_prep_attempts" -lt "$GIT_PREP_MAX_RETRIES" ]; then
|
|
950
|
-
_git_prep_attempts=$((_git_prep_attempts + 1))
|
|
951
|
-
echo "[pool] git prep previously failed — retrying (attempt ${_git_prep_attempts}/${GIT_PREP_MAX_RETRIES})..."
|
|
952
|
-
rm -f "$GIT_READY_MARKER" "$GIT_FAILED_MARKER"
|
|
953
|
-
prepare_workspace_git &
|
|
954
|
-
else
|
|
955
|
-
echo "[pool] git prep failed ${GIT_PREP_MAX_RETRIES} times — giving up on retries, letting agent surface the failure."
|
|
956
|
-
fi
|
|
957
|
-
fi
|
|
958
|
-
conveyor-agent 2>&1 | tee -a /tmp/claudespace-agent.log
|
|
959
|
-
_exit_code=${PIPESTATUS[0]}
|
|
960
|
-
if [ "$_exit_code" -eq 0 ]; then
|
|
961
|
-
echo "[pool] agent exited cleanly (code 0), shutting down pod."
|
|
962
|
-
exit 0
|
|
963
|
-
fi
|
|
964
|
-
AGENT_CRASH_ATTEMPTS=$((AGENT_CRASH_ATTEMPTS + 1))
|
|
965
|
-
if [ "$AGENT_CRASH_ATTEMPTS" -ge "$AGENT_CRASH_MAX" ]; then
|
|
966
|
-
echo "[pool] agent crashed (code $_exit_code) — attempt cap ${AGENT_CRASH_MAX} reached, giving up."
|
|
967
|
-
report_agent_crash "$_exit_code" "$AGENT_CRASH_ATTEMPTS" true
|
|
968
|
-
exit 1
|
|
969
|
-
fi
|
|
970
|
-
report_agent_crash "$_exit_code" "$AGENT_CRASH_ATTEMPTS" false
|
|
971
|
-
if [ -f "$GIT_FAILED_MARKER" ] && [ "$_git_prep_attempts" -ge "$GIT_PREP_MAX_RETRIES" ]; then
|
|
972
|
-
echo "[pool] agent crashed (code $_exit_code) after git prep exhausted retries, backing off (60s, attempt ${AGENT_CRASH_ATTEMPTS}/${AGENT_CRASH_MAX})..."
|
|
973
|
-
sleep 60
|
|
974
|
-
else
|
|
975
|
-
echo "[pool] agent crashed (code $_exit_code), retrying in 10s (attempt ${AGENT_CRASH_ATTEMPTS}/${AGENT_CRASH_MAX})..."
|
|
976
|
-
sleep 10
|
|
977
|
-
fi
|
|
978
|
-
done
|
|
10
|
+
conveyor-agent boot --preflight-update || echo "[shim] preflight failed; booting baked agent"
|
|
11
|
+
exec conveyor-agent boot
|