@rallycry/conveyor-agent 10.13.13 → 10.13.15
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-YRC57EYG.js → chunk-LZM5OKAC.js} +2 -68
- package/dist/chunk-LZM5OKAC.js.map +1 -0
- package/dist/{chunk-2K6RRTQC.js → chunk-RSIW2UCR.js} +4 -180
- package/dist/chunk-RSIW2UCR.js.map +1 -0
- package/dist/{chunk-TB5SQIGX.js → chunk-SAHOFQBQ.js} +216 -457
- package/dist/chunk-SAHOFQBQ.js.map +1 -0
- package/dist/{chunk-IE7EOKMD.js → chunk-VKZ5W2VO.js} +1 -1
- package/dist/chunk-VKZ5W2VO.js.map +1 -0
- package/dist/cli.js +35 -75
- package/dist/cli.js.map +1 -1
- package/dist/{client-ICIWKSK2.js → client-VQIBGPE6.js} +3 -3
- package/dist/index.d.ts +27 -77
- package/dist/index.js +5 -17
- package/dist/index.js.map +1 -1
- package/dist/{protocol-QJRHTTMQ.js → protocol-IHRTO5C4.js} +2 -2
- package/dist/{server-UGE67KIZ.js → server-TVIXWCAI.js} +7 -72
- package/dist/server-TVIXWCAI.js.map +1 -0
- package/package.json +1 -1
- package/runtime/entrypoint.sh +34 -98
- package/dist/chunk-2K6RRTQC.js.map +0 -1
- package/dist/chunk-IE7EOKMD.js.map +0 -1
- package/dist/chunk-TB5SQIGX.js.map +0 -1
- package/dist/chunk-YRC57EYG.js.map +0 -1
- package/dist/server-UGE67KIZ.js.map +0 -1
- /package/dist/{client-ICIWKSK2.js.map → client-VQIBGPE6.js.map} +0 -0
- /package/dist/{protocol-QJRHTTMQ.js.map → protocol-IHRTO5C4.js.map} +0 -0
package/runtime/entrypoint.sh
CHANGED
|
@@ -334,13 +334,6 @@ boot_mark "env injected — entering user-home wiring"
|
|
|
334
334
|
|
|
335
335
|
export ANTHROPIC_API_KEY=$(echo "${BOOTSTRAP_JSON}" | jq -r '.anthropicKey // empty')
|
|
336
336
|
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gcpToken // empty')
|
|
337
|
-
# WorkPreservation GCS tier (spec §Durability & snapshots): the upload URL is
|
|
338
|
-
# long-lived (used for the pod's whole lifetime of periodic captures + sleep
|
|
339
|
-
# finalize); the download URL is present only when a snapshot exists and is
|
|
340
|
-
# consumed once by restoreOnBoot.
|
|
341
|
-
CONVEYOR_SNAPSHOT_UPLOAD_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUploadUrl // empty')
|
|
342
|
-
CONVEYOR_SNAPSHOT_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUrl // empty')
|
|
343
|
-
export CONVEYOR_SNAPSHOT_UPLOAD_URL CONVEYOR_SNAPSHOT_URL
|
|
344
337
|
unset BOOTSTRAP_JSON
|
|
345
338
|
# POD_BOOTSTRAP_TOKEN is deliberately NOT unset: it is the credential-refresh
|
|
346
339
|
# key. The agent re-polls GET /api/v3/pods/bootstrap with it to swap in a
|
|
@@ -688,96 +681,29 @@ reset_tracked_repo_changes_before_assignment_checkout() {
|
|
|
688
681
|
}
|
|
689
682
|
|
|
690
683
|
# Shared by both the pod-image branch and the repo-present-non-pod-image branch
|
|
691
|
-
# of prepare_workspace_git: refresh the remote token
|
|
692
|
-
#
|
|
693
|
-
#
|
|
694
|
-
#
|
|
695
|
-
#
|
|
696
|
-
#
|
|
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.
|
|
697
691
|
sync_task_branch_to_repo() {
|
|
698
692
|
# Guarded so a set-url failure writes the failed marker instead of aborting
|
|
699
693
|
# the backgrounded subshell (which would leave the agent waiting forever).
|
|
700
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
|
|
701
|
-
echo "[pool] ERROR: remote set-url failed
|
|
702
|
-
printf '%s' "
|
|
695
|
+
echo "[pool] ERROR: remote set-url failed" >&2
|
|
696
|
+
printf '%s' "remote set-url failed" > "$GIT_FAILED_MARKER"
|
|
703
697
|
return 1
|
|
704
698
|
fi
|
|
705
|
-
#
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if ! _fetch_dev_out=$(git -C repo fetch origin "+refs/heads/${DEV_BRANCH}:refs/remotes/origin/${DEV_BRANCH}" 2>&1); then
|
|
709
|
-
echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
|
|
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
|
|
710
702
|
fi
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if [ -n "${CHECKOUT_REF}" ]; then
|
|
717
|
-
echo "[pool] Fetching checkout ref ${CHECKOUT_REF} for review branch ${BRANCH}..."
|
|
718
|
-
if ! _fetch_checkout_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
|
|
719
|
-
echo "[pool] ERROR: Repo fetch failed for checkout ref '${CHECKOUT_REF}': ${_fetch_checkout_out}" >&2
|
|
720
|
-
printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
721
|
-
return 1
|
|
722
|
-
fi
|
|
723
|
-
if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
|
|
724
|
-
echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
|
|
725
|
-
printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
|
|
726
|
-
return 1
|
|
727
|
-
fi
|
|
728
|
-
else
|
|
729
|
-
# Fetch the task branch into a local ref explicitly so we can check it out.
|
|
730
|
-
# If the branch doesn't exist on origin, create it from the base branch.
|
|
731
|
-
# Note: if BRANCH equals DEV_BRANCH (deferred-branch case), the fetch above
|
|
732
|
-
# already populated the ref and this is a no-op.
|
|
733
|
-
if ! _fetch_out=$(git -C repo fetch origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>&1); then
|
|
734
|
-
# Check if the error is specifically due to the remote ref not existing
|
|
735
|
-
if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
|
|
736
|
-
echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
|
|
737
|
-
# Create the branch locally from the base branch
|
|
738
|
-
if ! _create_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
|
|
739
|
-
echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
|
|
740
|
-
printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
741
|
-
return 1
|
|
742
|
-
fi
|
|
743
|
-
# Push the new branch to origin with -u to set up tracking
|
|
744
|
-
if ! _push_out=$(git -C repo push -u origin "${BRANCH}" 2>&1); then
|
|
745
|
-
echo "[pool] ERROR: Failed to push new branch '${BRANCH}': ${_push_out}" >&2
|
|
746
|
-
printf '%s' "push new branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
747
|
-
return 1
|
|
748
|
-
fi
|
|
749
|
-
echo "[pool] Created and pushed new branch '${BRANCH}' from '${DEV_BRANCH}'"
|
|
750
|
-
else
|
|
751
|
-
# Other git error (auth, network, etc.) — fail loud as before
|
|
752
|
-
echo "[pool] ERROR: Repo fetch failed for branch '${BRANCH}': ${_fetch_out}" >&2
|
|
753
|
-
printf '%s' "fetch branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
754
|
-
return 1
|
|
755
|
-
fi
|
|
756
|
-
fi
|
|
757
|
-
# Use `checkout -B` to create or reset the local branch tracking origin.
|
|
758
|
-
# This leaves HEAD attached to a real branch (not detached) so `git status`,
|
|
759
|
-
# `git push`, and any tool that keys off branch state work correctly.
|
|
760
|
-
if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
|
|
761
|
-
echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
|
|
762
|
-
printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
|
|
763
|
-
return 1
|
|
764
|
-
fi
|
|
765
|
-
fi
|
|
766
|
-
|
|
767
|
-
# Merge latest dev into the task branch so the agent starts on up-to-date
|
|
768
|
-
# base code. Skip if the branch already contains origin/dev (fast-path)
|
|
769
|
-
# or if BRANCH is the dev branch itself.
|
|
770
|
-
if [ "${BRANCH}" != "${DEV_BRANCH}" ] && git -C repo rev-parse "origin/${DEV_BRANCH}" >/dev/null 2>&1; then
|
|
771
|
-
if git -C repo merge-base --is-ancestor "origin/${DEV_BRANCH}" HEAD; then
|
|
772
|
-
echo "[pool] Branch already up-to-date with origin/${DEV_BRANCH}"
|
|
773
|
-
elif ! _merge_out=$(git -C repo merge "origin/${DEV_BRANCH}" --no-edit 2>&1); then
|
|
774
|
-
echo "[pool] WARN: merge origin/${DEV_BRANCH} failed, aborting: ${_merge_out}" >&2
|
|
775
|
-
git -C repo merge --abort 2>/dev/null || true
|
|
776
|
-
else
|
|
777
|
-
echo "[pool] Merged origin/${DEV_BRANCH} into ${BRANCH}"
|
|
778
|
-
fi
|
|
779
|
-
fi
|
|
780
|
-
echo "[pool] Repo updated to ${BRANCH}@$(git -C repo rev-parse --short HEAD)"
|
|
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}"
|
|
781
707
|
return 0
|
|
782
708
|
}
|
|
783
709
|
|
|
@@ -803,11 +729,10 @@ prepare_workspace_git() {
|
|
|
803
729
|
mark_git_ready
|
|
804
730
|
elif [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
|
|
805
731
|
# Non-image but repo already present (e.g. a baked image running without
|
|
806
|
-
# CONVEYOR_POD_IMAGE set).
|
|
807
|
-
#
|
|
808
|
-
#
|
|
809
|
-
# branch
|
|
810
|
-
# branch so this path ends up in the same checked-out-on-task-branch state.
|
|
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.
|
|
811
736
|
echo "[pool] Repo present (non-pod-image) — updating repo to latest (branch=${BRANCH})..."
|
|
812
737
|
if ! sync_task_branch_to_repo; then
|
|
813
738
|
return 1
|
|
@@ -840,9 +765,10 @@ prepare_workspace_git() {
|
|
|
840
765
|
# branch. A naive `clone --branch <task>` dies with "Remote branch not
|
|
841
766
|
# found" when the task branch has never been pushed to origin, which
|
|
842
767
|
# crash-loops the pod on every fresh task. sync_task_branch_to_repo below
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
#
|
|
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".
|
|
846
772
|
if ! _clone_out=$(git clone --single-branch --branch "${BASE_BRANCH}" \
|
|
847
773
|
"https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
|
|
848
774
|
repo 2>&1); then
|
|
@@ -1007,6 +933,13 @@ _git_prep_attempts=0
|
|
|
1007
933
|
AGENT_CRASH_ATTEMPTS=0
|
|
1008
934
|
AGENT_CRASH_MAX=3
|
|
1009
935
|
|
|
936
|
+
# From here on the pod's liveness probe (liveness.sh, agent container only) has
|
|
937
|
+
# authority: this marker tells it bootstrap/standby/git-prep are over, so a
|
|
938
|
+
# missing conveyor-agent process now means a dead supervisor loop rather than a
|
|
939
|
+
# pod that hasn't launched yet. The restarting marker (touched fresh on every
|
|
940
|
+
# crash below) keeps the probe green across the supervised backoff sleeps.
|
|
941
|
+
touch /tmp/conveyor-agent-launching
|
|
942
|
+
|
|
1010
943
|
report_agent_crash() {
|
|
1011
944
|
# $1 = exit code, $2 = attempt, $3 = final (true/false). Best-effort.
|
|
1012
945
|
curl -s -m 10 -X POST \
|
|
@@ -1035,6 +968,9 @@ while true; do
|
|
|
1035
968
|
echo "[pool] agent exited cleanly (code 0), shutting down pod."
|
|
1036
969
|
exit 0
|
|
1037
970
|
fi
|
|
971
|
+
# Fresh mtime on every crash: liveness.sh grants a 90s grace window from
|
|
972
|
+
# this timestamp so the 10s/60s backoff sleeps below never trip the probe.
|
|
973
|
+
touch /tmp/conveyor-agent-restarting
|
|
1038
974
|
AGENT_CRASH_ATTEMPTS=$((AGENT_CRASH_ATTEMPTS + 1))
|
|
1039
975
|
if [ "$AGENT_CRASH_ATTEMPTS" -ge "$AGENT_CRASH_MAX" ]; then
|
|
1040
976
|
echo "[pool] agent crashed (code $_exit_code) — attempt cap ${AGENT_CRASH_MAX} reached, giving up."
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/harness/pty/pty-support.ts","../src/utils/sleep.ts","../src/runner/work-preservation/snapshot-tar.ts","../src/runner/work-preservation/snapshot-artifact.ts","../src/setup/commands.ts"],"sourcesContent":["/**\n * Pure support helpers for PtySession — node-pty process loading, prompt-byte\n * encoding, the per-turn options projection, and the AskUserQuestion parser.\n * Extracted from `session.ts` so the orchestrator stays focused on lifecycle;\n * everything here is side-effect-free (or a thin wrapper over one) and unit\n * tested in isolation.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport type { HarnessQueryOptions, HarnessUserQuestion } from \"../types.js\";\n\n// Cap on the rolling tail of raw terminal output retained for diagnostics when\n// `claude` exits without a result. Bounded so a long-running session can't grow\n// this unboundedly; only the most recent bytes (where a failure surfaces) matter.\nexport const MAX_DIAGNOSTIC_OUTPUT = 4000;\n\n// Cap on transcript events buffered while no turn is draining (the parked\n// window between a completed turn and the next beginTurn / a passive turn).\n// Bounded, drop-oldest — a stale buffered `result` must never terminate a\n// fresh turn, and beginTurn clears the buffer anyway.\nexport const MAX_BETWEEN_TURN_BUFFER = 500;\n\n/** Settle window between the paste write and the submitting Enter write. */\nexport const SUBMIT_SETTLE_MS = 300;\n// Re-press cadence/bounds for the submit nudge (see armSubmitNudge). Fast\n// presses cover the Enter-swallowed-at-startup race; the slow phase covers\n// startup dialogs that render tens of seconds in on a cold pod (folder\n// trust, onboarding) — observed live 2026-07-02 parking code reviews ~13min+\n// when the trust dialog mounted after the fast window had expired.\nexport const SUBMIT_NUDGE_INTERVAL_MS = 2000;\nexport const SUBMIT_NUDGE_MAX_PRESSES = 5;\nexport const SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5000;\nexport const SUBMIT_NUDGE_WINDOW_MS = 90_000;\n\n// Plan-dialog auto-accept press cadence/bounds (see armPlanDialogAutoAccept).\n// The dialog usually renders within a second of the ExitPlanMode hook verdict,\n// but the window is deliberately wide: the only stop signal is the ExitPlanMode\n// PostToolUse envelope, and a missed dialog parks an auto card indefinitely\n// (observed live 2026-07-07: an approval dialog sat 42 minutes until a human\n// pressed Enter).\nexport const PLAN_DIALOG_FIRST_PRESS_MS = 700;\nexport const PLAN_DIALOG_INTERVAL_MS = 1500;\nexport const PLAN_DIALOG_SLOW_INTERVAL_MS = 5000;\nexport const PLAN_DIALOG_FAST_WINDOW_MS = 10_000;\nexport const PLAN_DIALOG_WINDOW_MS = 90_000;\n\nfunction envMs(name: string, fallback: number): number {\n const raw = Number(process.env[name]);\n return Number.isFinite(raw) && raw > 0 ? raw : fallback;\n}\n\nexport function resolveSubmitSettleMs(): number {\n return envMs(\"CONVEYOR_PTY_SUBMIT_SETTLE_MS\", SUBMIT_SETTLE_MS);\n}\n\n/**\n * Effective submit-nudge timing. The exported constants above are the production\n * defaults; `CONVEYOR_PTY_NUDGE_INTERVAL_MS` / `_SLOW_INTERVAL_MS` / `_WINDOW_MS`\n * override them. Resolved at nudge-arm time (not import) so an integration test\n * can drive the two-phase fast→slow behavior in milliseconds instead of ~15s of\n * real wall-clock. `maxPresses` is deliberately NOT overridable — the fast→slow\n * phase-boundary count is the behavior under test.\n */\nexport function resolveSubmitNudgeTiming(): {\n intervalMs: number;\n slowIntervalMs: number;\n maxPresses: number;\n windowMs: number;\n} {\n return {\n intervalMs: envMs(\"CONVEYOR_PTY_NUDGE_INTERVAL_MS\", SUBMIT_NUDGE_INTERVAL_MS),\n slowIntervalMs: envMs(\"CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS\", SUBMIT_NUDGE_SLOW_INTERVAL_MS),\n maxPresses: SUBMIT_NUDGE_MAX_PRESSES,\n windowMs: envMs(\"CONVEYOR_PTY_NUDGE_WINDOW_MS\", SUBMIT_NUDGE_WINDOW_MS),\n };\n}\n\nexport function resolvePlanDialogTiming(): {\n firstPressMs: number;\n intervalMs: number;\n slowIntervalMs: number;\n fastWindowMs: number;\n windowMs: number;\n} {\n return {\n firstPressMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS\", PLAN_DIALOG_FIRST_PRESS_MS),\n intervalMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS\", PLAN_DIALOG_INTERVAL_MS),\n slowIntervalMs: envMs(\n \"CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS\",\n PLAN_DIALOG_SLOW_INTERVAL_MS,\n ),\n fastWindowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS\", PLAN_DIALOG_FAST_WINDOW_MS),\n windowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS\", PLAN_DIALOG_WINDOW_MS),\n };\n}\n\n/**\n * The per-turn subset of HarnessQueryOptions — everything that legitimately\n * changes from one turn to the next while the same `claude` process serves them\n * all. The process-level fields (model, cwd, sessionId, appendSystemPrompt, …)\n * are fixed at spawn and captured separately in the spawn options.\n */\nexport interface TurnOptions {\n canUseTool?: HarnessQueryOptions[\"canUseTool\"];\n promptDelivery?: \"submit\" | \"prefill\";\n planDialogAutoAccept?: boolean;\n abortController?: AbortController;\n}\n\nexport function turnOptionsFrom(options: HarnessQueryOptions): TurnOptions {\n return {\n canUseTool: options.canUseTool,\n promptDelivery: options.promptDelivery,\n planDialogAutoAccept: options.planDialogAutoAccept,\n abortController: options.abortController,\n };\n}\n\nexport interface PtyProcess {\n onData(listener: (data: string) => void): void;\n onExit(listener: (event: { exitCode: number }) => void): void;\n write(data: string): void;\n resize(cols: number, rows: number): void;\n kill(signal?: string): void;\n}\n\nexport interface PtySpawnOptions {\n name: string;\n cols: number;\n rows: number;\n cwd: string;\n env: Record<string, string>;\n}\n\nexport type PtySpawn = (file: string, args: string[], options: PtySpawnOptions) => PtyProcess;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction extractSpawn(mod: unknown): PtySpawn | null {\n if (!isRecord(mod)) return null;\n if (typeof mod.spawn === \"function\") return mod.spawn as PtySpawn;\n const def = mod.default;\n if (isRecord(def) && typeof def.spawn === \"function\") return def.spawn as PtySpawn;\n return null;\n}\n\nexport async function loadPtySpawn(): Promise<PtySpawn> {\n const mod: unknown = await import(\"node-pty\");\n const spawn = extractSpawn(mod);\n if (!spawn) throw new Error(\"node-pty: spawn export not found\");\n return spawn;\n}\n\n/**\n * The spawn implementation for this pod topology: node-pty locally, or the\n * workbench launcher's remote PTY in split-mode pods (the CLI must run in the\n * workbench container, next to the repo and inside the restartable cgroup).\n * Same PtySpawn contract either way — PtySession is topology-blind.\n */\nexport async function resolvePtySpawn(): Promise<PtySpawn> {\n const { workbenchEnabled } = await import(\"../../workbench/mode.js\");\n if (!workbenchEnabled()) return loadPtySpawn();\n const { getWorkbenchClient } = await import(\"../../workbench/client.js\");\n return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);\n}\n\n/**\n * Base directory for a PTY session's control files (settings.json,\n * mcp-config, hook socket). In split-mode pods this MUST be the shared\n * emptyDir — the spawned CLI reads these paths from the workbench container,\n * and the hook helper connects back over the unix socket, which crosses\n * containers only on a shared volume.\n */\nexport function sessionTempBase(): string {\n return process.env.CONVEYOR_SHARED_DIR ?? tmpdir();\n}\n\nexport function inheritedEnv(socketPath?: string): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [key, value] of Object.entries(process.env)) {\n if (typeof value === \"string\") env[key] = value;\n }\n // Do NOT force CLAUDE_CONFIG_DIR. Forcing it to ~/.claude makes the CLI read\n // <dir>/.claude.json — a fresh, un-onboarded config — instead of the user's\n // real ~/.claude.json, which re-triggers the first-run theme picker + login\n // even though valid credentials already exist in ~/.claude/. The loop above\n // already propagates an explicitly-set CLAUDE_CONFIG_DIR (pods/codespaces),\n // and the config dir is unchanged either way, so transcript tailing (which\n // uses claudeConfigHome()) still resolves to the same projects/ directory.\n // Belt-and-braces against the ANTHROPIC_API_KEY leak: when a subscription\n // OAuth token is in play, the TUI authenticates via the synthesized\n // credentials.json — an inherited ANTHROPIC_API_KEY would be treated as an\n // EXTERNAL API key and, once interactively approved, poison auth (the token\n // is sent via x-api-key → 401 forever). The API bundle already stops serving\n // the OAuth token under that name; stripping it here means no upstream leak\n // can reach the spawned CLI regardless. The two auth modes are mutually\n // exclusive (see session-runner's onApiKeyUpdate), so a real api_key launch\n // has no CLAUDE_CODE_OAUTH_TOKEN and keeps its ANTHROPIC_API_KEY.\n if (env.CLAUDE_CODE_OAUTH_TOKEN) {\n delete env.ANTHROPIC_API_KEY;\n }\n if (socketPath) {\n env.CONVEYOR_HOOK_SOCKET = socketPath;\n }\n // The conveyor MCP tool handlers proxy over the agent's API socket, and a\n // mid-flap call can block up to ~50s (20s reconnect-wait + 30s ack) before\n // failing with a retryable error. The CLI's default MCP timeouts are\n // shorter — hitting them made it abandon the MCP session mid-flap\n // (2026-07-08 wedge). Default both timeouts past the worst-case hang;\n // operator-set values win via the inherit loop above.\n env.MCP_TIMEOUT ??= \"60000\";\n env.MCP_TOOL_TIMEOUT ??= \"180000\";\n return env;\n}\n\n/**\n * The bracketed-paste bytes written to the CLI's stdin to deliver a prompt.\n * The text is wrapped in paste markers so embedded newlines land as literal\n * lines in the input box instead of submitting it early. The submitting Enter\n * is deliberately NOT part of these bytes: deliverPrompt() sends it as a\n * separate write after a settle window, because an Enter folded into the same\n * chunk as the paste can be swallowed by the CLI's paste-burst handling —\n * observed in production as an auto-mode prompt parked unsubmitted in the\n * input box. \"prefill\" delivery never sends an Enter at all.\n */\nexport function buildPromptBytes(text: string): string {\n return `\\x1b[200~${text}\\x1b[201~`;\n}\n\n/**\n * Render structured (multimodal) prompt content as pasteable text. Image\n * blocks are replaced with a tool reference — their base64 payload must NEVER\n * be pasted into the TUI input box. Upstream prompt builders already skip\n * image blocks for the PTY harness; this is the last line of defense.\n */\nexport function renderPromptContentText(content: unknown[]): string {\n return content\n .map((block) => {\n const b = block as { type?: string; text?: string };\n if (b?.type === \"text\" && typeof b.text === \"string\") return b.text;\n if (b?.type === \"image\") {\n return `[Image attachment — use list_task_files / get_attachment to view]`;\n }\n return JSON.stringify(block);\n })\n .join(\"\\n\\n\");\n}\n\nexport { sleep } from \"../../utils/sleep.js\";\n\n/** Current size of the transcript, or 0 if it does not exist yet. */\nexport async function transcriptSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Defensive extraction of AskUserQuestion's `questions` input from the hook\n * payload. Malformed entries are dropped rather than thrown — the event is\n * observe-only, so an empty list still arms the waiting_for_input report.\n */\nexport function parseUserQuestions(input: Record<string, unknown>): HarnessUserQuestion[] {\n if (!Array.isArray(input.questions)) return [];\n const questions: HarnessUserQuestion[] = [];\n for (const entry of input.questions) {\n if (!isRecord(entry)) continue;\n if (typeof entry.question !== \"string\") continue;\n const options = Array.isArray(entry.options)\n ? entry.options\n .filter(isRecord)\n .filter((o) => typeof o.label === \"string\")\n .map((o) => ({\n label: o.label as string,\n description: typeof o.description === \"string\" ? o.description : \"\",\n }))\n : [];\n questions.push({\n question: entry.question,\n header: typeof entry.header === \"string\" ? entry.header : \"\",\n options,\n ...(typeof entry.multiSelect === \"boolean\" ? { multiSelect: entry.multiSelect } : {}),\n });\n }\n return questions;\n}\n","export function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import { execFile } from \"node:child_process\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { promisify } from \"node:util\";\nimport { createGzip } from \"node:zlib\";\nimport * as tar from \"tar\";\nimport { planSnapshotFiles, type GitSurface } from \"./snapshot-artifact.js\";\n\n/**\n * In-archive manifest binding a snapshot to the HEAD it was captured on and\n * carrying the deletion list. Lives ONLY inside the tar (assembled in a temp\n * staging dir) — never written into the workspace tree.\n */\nexport const SNAPSHOT_MANIFEST_NAME = \".conveyor-snapshot-manifest.json\";\n\n/**\n * Legacy in-archive name for the postgres dump that pre-#2623 agents bundled\n * into the snapshot tar. Sidecar state is no longer durable, so we neither\n * write nor restore this anymore — but a snapshot captured by an OLDER agent\n * (still sitting on the fixed per-workspace GCS object until the next finalize)\n * can carry one. Extraction must keep SKIPPING this name so the stale dump can\n * never land in the worktree and ride the wip stash's `git add -A` to GitHub.\n */\nconst LEGACY_PG_DUMP_FILENAME = \".conveyor-pgdump.sql\";\n\nexport interface SnapshotManifest {\n /** `git rev-parse HEAD` at capture time — restore refuses to extract over a\n * different HEAD (a stale delta over a newer clone clobbers newer work). */\n head: string;\n /** Repo-relative paths restore must delete after extraction. */\n deletions: string[];\n /** Epoch ms when the tar was BUILT (not uploaded) — also sent as the\n * `x-snapshot-captured-at` header so the API can refuse out-of-order PUTs. */\n capturedAt: number;\n}\n\n// git status --porcelain=v1 -z default maxBuffer (1MB) is too small for large\n// dirty trees; 64MB of NUL-delimited paths covers any realistic worktree.\nconst STATUS_MAX_BUFFER = 64 * 1024 * 1024;\n\n/**\n * Parse `git status --porcelain=v1 -z -uall` output. NUL-delimited and\n * unquoted, so unicode/space/quote filenames arrive verbatim (the LF-based\n * parse broke them via C-style quoting). Rename/copy entries deliver TWO\n * NUL-separated fields — `XY <new>\\0<orig>\\0`; the NEW path is included and,\n * for renames, the ORIGINAL path joins the deletion list (it exists in HEAD\n * but no longer on disk).\n */\nexport function parseStatusPorcelainZ(raw: string): { includes: string[]; deletions: string[] } {\n const includes: string[] = [];\n const deletions: string[] = [];\n const tokens = raw.split(\"\\0\");\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (!token || token.length < 4) continue;\n const x = token[0];\n const y = token[1];\n const filePath = token.slice(3);\n if (x === \"R\" || x === \"C\") {\n const origPath = tokens[++i];\n if (x === \"R\" && origPath) deletions.push(origPath);\n }\n // Ignored entries (only present with --ignored) never enter the plan.\n if (x === \"!\") continue;\n // The file is absent from the worktree when the worktree (Y) column is D\n // (covers ` D`, `AD`, `MD`, `RD`, `DD`), or on a staged-only deletion\n // (`D ` — deleted from index, gone from disk, worktree column blank).\n const missingFromWorktree = y === \"D\" || (x === \"D\" && y === \" \");\n if (missingFromWorktree) deletions.push(filePath);\n else includes.push(filePath);\n }\n return { includes, deletions };\n}\n\n// Async git (execFile, no shell): a synchronous status/rev-parse here would\n// freeze the agent's event loop for the duration — on an IO-starved pod (the\n// exact moment snapshots run) that starves the socket heartbeat and gets the\n// session restarted. See runner/git-utils.ts for the full rationale.\nconst execFileAsync = promisify(execFile);\nconst GIT_TIMEOUT_MS = 120_000;\n\n/** Single `git status --porcelain=v1 -z -uall` read feeding both GitSurface\n * views — includes and deletions must come from one consistent status read.\n * Loaded up-front (async); the returned surface serves it synchronously. */\nasync function realGitSurface(cwd: string): Promise<GitSurface> {\n const { stdout } = await execFileAsync(\"git\", [\"status\", \"--porcelain=v1\", \"-z\", \"-uall\"], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: STATUS_MAX_BUFFER,\n });\n const cached = parseStatusPorcelainZ(stdout.toString());\n return {\n trackedAndUntracked: () => cached.includes,\n deletedSinceHead: () => cached.deletions,\n };\n}\n\nasync function gitHead(cwd: string): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n });\n return stdout.toString().trim() || null;\n } catch {\n return null;\n }\n}\n\nexport interface SnapshotTarResult {\n /** Gzipped tar on disk inside a private temp staging dir. Callers stream it\n * (never buffer the whole archive) and MUST call cleanup() when done. */\n tarPath: string;\n sizeBytes: number;\n fileCount: number;\n deletionCount: number;\n /** Epoch ms the tar was built — the capture generation marker. */\n capturedAt: number;\n cleanup(): Promise<void>;\n}\n\n/**\n * Assemble the snapshot archive: the manifest staged in a temp dir, worktree\n * files appended from the repo. The archive is written to disk and returned as\n * a path so upload can stream it — the tar is never held in memory (pods have\n * an OOM history; a single whole-tar Buffer was up to 500MB).\n */\nexport async function buildSnapshotTar(cwd: string): Promise<SnapshotTarResult> {\n const staging = await mkdtemp(path.join(tmpdir(), \"conveyor-snapshot-\"));\n const cleanup = async (): Promise<void> => {\n await rm(staging, { recursive: true, force: true }).catch(() => {});\n };\n\n try {\n const plan = planSnapshotFiles(await realGitSurface(cwd));\n const head = await gitHead(cwd);\n if (!head) throw new Error(\"cannot snapshot a repo without a resolvable HEAD\");\n const capturedAt = Date.now();\n\n const manifest: SnapshotManifest = {\n head,\n deletions: plan.deletions,\n capturedAt,\n };\n await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), \"utf8\");\n\n const stagedEntries = [SNAPSHOT_MANIFEST_NAME];\n\n // tar.replace can't operate on a compressed archive, so: create the\n // uncompressed tar from the staging entries, append the repo files with\n // cwd = the repo, then gzip file-to-file (all streaming).\n const rawTarPath = path.join(staging, \"snapshot.tar\");\n await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);\n if (plan.include.length > 0) {\n await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);\n }\n\n const tarPath = path.join(staging, \"snapshot.tar.gz\");\n await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));\n await rm(rawTarPath, { force: true });\n\n const { size } = await stat(tarPath);\n return {\n tarPath,\n sizeBytes: size,\n fileCount: plan.include.length,\n deletionCount: plan.deletions.length,\n capturedAt,\n cleanup,\n };\n } catch (err) {\n await cleanup();\n throw err;\n }\n}\n\nexport type SnapshotExtractResult =\n | {\n status: \"extracted\";\n filesExtracted: number;\n deletionsApplied: number;\n }\n /** Manifest HEAD differs from the current clone's HEAD — extracting would\n * smear an old delta over newer commits. The tree is NOT touched. */\n | { status: \"stale-head\"; snapshotHead: string; currentHead: string | null }\n /** Missing/unreadable manifest — not a trustworthy snapshot. Tree untouched. */\n | { status: \"invalid\" };\n\n/** Repo-relative deletion entries must resolve inside the workspace — a\n * malicious/corrupt manifest must never delete outside cwd. */\nfunction isWithinWorkspace(cwd: string, rel: string): boolean {\n if (!rel || path.isAbsolute(rel)) return false;\n const root = path.resolve(cwd);\n const abs = path.resolve(root, rel);\n return abs !== root && abs.startsWith(root + path.sep);\n}\n\n/**\n * Single `tar.list` pass over the archive to parse the manifest. Returns the\n * validated manifest, or null when it is missing/unreadable/lacks a HEAD.\n */\nasync function readSnapshotArchive(tarPath: string): Promise<SnapshotManifest | null> {\n let manifestRaw: Buffer | null = null;\n\n try {\n await tar.list({\n file: tarPath,\n onReadEntry: (entry) => {\n if (entry.path === SNAPSHOT_MANIFEST_NAME) {\n const chunks: Buffer[] = [];\n entry.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n entry.on(\"end\", () => {\n manifestRaw = Buffer.concat(chunks);\n });\n }\n },\n });\n } catch {\n return null;\n }\n\n if (!manifestRaw) return null;\n try {\n const parsed = JSON.parse((manifestRaw as Buffer).toString(\"utf8\")) as SnapshotManifest;\n if (typeof parsed.head !== \"string\" || !parsed.head) return null;\n return {\n head: parsed.head,\n deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],\n capturedAt: typeof parsed.capturedAt === \"number\" ? parsed.capturedAt : 0,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Extract a snapshot archive (path to a .tar.gz on disk) into the workspace.\n * Order matters: the manifest is read and the HEAD binding checked BEFORE any\n * tree mutation — a stale or invalid snapshot leaves the tree byte-identical.\n * A valid snapshot with zero files/deletions is still a SUCCESS (a clean tree\n * at capture time is a legitimate snapshot, not a failure).\n */\nexport async function extractSnapshotTar(\n cwd: string,\n tarPath: string,\n): Promise<SnapshotExtractResult> {\n const manifest = await readSnapshotArchive(tarPath);\n if (!manifest) return { status: \"invalid\" };\n\n const currentHead = await gitHead(cwd);\n if (manifest.head !== currentHead) {\n return { status: \"stale-head\", snapshotHead: manifest.head, currentHead };\n }\n\n let filesExtracted = 0;\n await tar.extract({\n file: tarPath,\n cwd,\n filter: (entryPath) => {\n // Manifest is archive metadata and never lands in cwd. A legacy pg dump\n // (bundled by pre-#2623 agents) is likewise skipped — sidecar state is no\n // longer restored, and a dump in the worktree would ride the next wip\n // stash's `git add -A` to GitHub.\n if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {\n return false;\n }\n filesExtracted++;\n return true;\n },\n });\n\n let deletionsApplied = 0;\n for (const rel of manifest.deletions) {\n if (typeof rel !== \"string\" || !isWithinWorkspace(cwd, rel)) continue;\n await rm(path.resolve(cwd, rel), { recursive: true, force: true });\n deletionsApplied++;\n }\n\n return { status: \"extracted\", filesExtracted, deletionsApplied };\n}\n\n/** Test seam: read a manifest back out of an archive without extracting. */\nexport function readSnapshotManifest(tarPath: string): Promise<SnapshotManifest | null> {\n return readSnapshotArchive(tarPath);\n}\n\n/** Test seam: list entry paths in an archive without extracting. */\nexport async function listSnapshotEntries(tarPath: string): Promise<string[]> {\n const entries: string[] = [];\n await tar.list({\n file: tarPath,\n onReadEntry: (entry) => {\n entries.push(String(entry.path));\n },\n });\n return entries;\n}\n","/**\n * Pure planning logic for the WorkPreservation source-tree snapshot:\n * tracked + untracked files are included in the archive, while deleted paths\n * are recorded separately for restore-time removal.\n */\n\nexport interface GitSurface {\n trackedAndUntracked(): string[];\n deletedSinceHead(): string[];\n}\n\nexport interface SnapshotFilePlan {\n include: string[];\n deletions: string[];\n}\n\nexport function planSnapshotFiles(git: GitSurface): SnapshotFilePlan {\n const include = git.trackedAndUntracked();\n const includeSet = new Set(include);\n const deletions = [...new Set(git.deletedSinceHead())].filter((path) => !includeSet.has(path));\n return { include, deletions };\n}\n","import { spawn, execSync, type ChildProcess } from \"node:child_process\";\n\n/**\n * The structural surface of a start-command child the supervisor manages —\n * satisfied by a local ChildProcess and by the workbench client's\n * RemoteProcessHandle (split-mode pods, where the process lives in the\n * workbench container).\n */\nexport interface ManagedChildProcess extends NodeJS.EventEmitter {\n pid?: number | undefined;\n exitCode: number | null;\n kill(signal?: NodeJS.Signals | number): boolean;\n}\n\nconst PROCESS_TERMINATION_GRACE_MS = 5_000;\n\nfunction abortError(): Error {\n const error = new Error(\"Operation aborted\");\n error.name = \"AbortError\";\n return error;\n}\n\nfunction signalProcessGroup(child: ManagedChildProcess, signal: NodeJS.Signals): void {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n try {\n child.kill(signal);\n } catch {\n // The process already exited.\n }\n }\n}\n\nexport function terminateProcessGroup(\n child: ManagedChildProcess | ChildProcess,\n graceMs = PROCESS_TERMINATION_GRACE_MS,\n): Promise<void> {\n if (child.exitCode !== null) return Promise.resolve();\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n child.removeListener(\"exit\", finish);\n resolve();\n };\n const timer = setTimeout(() => {\n signalProcessGroup(child, \"SIGKILL\");\n finish();\n }, graceMs);\n timer.unref();\n child.once(\"exit\", finish);\n signalProcessGroup(child, \"SIGTERM\");\n });\n}\n\nexport function runSetupCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n signal?: AbortSignal,\n): Promise<void> {\n if (signal?.aborted) return Promise.reject(abortError());\n return new Promise((resolve, reject) => {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n let settled = false;\n let aborting = false;\n const cleanup = (): void => signal?.removeEventListener(\"abort\", onAbort);\n const settle = (error?: Error): void => {\n if (settled) return;\n settled = true;\n cleanup();\n if (error) reject(error);\n else resolve();\n };\n const onAbort = (): void => {\n if (settled || aborting) return;\n aborting = true;\n void terminateProcessGroup(child).then(() => settle(abortError()));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (signal?.aborted) onAbort();\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.on(\"close\", (code) => {\n if (aborting) return;\n settle(code === 0 ? undefined : new Error(`Setup command exited with code ${code}`));\n });\n\n child.on(\"error\", (err) => {\n if (!aborting) settle(err);\n });\n });\n}\n\nconst AUTH_TOKEN_TIMEOUT_MS = 30_000;\n\nexport function runAuthTokenCommand(cmd: string, userEmail: string, cwd: string): string | null {\n try {\n const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {\n cwd,\n timeout: AUTH_TOKEN_TIMEOUT_MS,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n env: { ...process.env },\n });\n const token = output.toString().trim();\n return token || null;\n } catch {\n return null;\n }\n}\n\nexport function runStartCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n): ChildProcess {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.unref();\n return child;\n}\n"],"mappings":"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","names":["spawn","path","stat","tmpdir","path","tmpdir","stat"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/workbench/protocol.ts"],"sourcesContent":["/**\n * Wire protocol for the workbench launcher — the loopback control channel\n * between the protected agent container and the restartable workbench\n * container (see docs/superpowers/specs/2026-07-16-workbench-container-split-design.md).\n *\n * Newline-delimited JSON frames over a plain TCP socket. Every connection\n * carries exactly ONE operation: the first client frame is a WorkbenchRequest\n * (with the shared token); everything after is a WorkbenchFrame in either\n * direction. Binary payloads (pty bytes, file contents, snapshot tarballs)\n * ride base64 `d` fields — intra-pod loopback makes the 33% inflation cheap,\n * and NDJSON keeps the framing trivially robust across partial reads.\n */\n\nimport crypto from \"node:crypto\";\nimport type { Socket } from \"node:net\";\n\n/** First frame of every connection (client → server). */\nexport type WorkbenchRequest =\n | { op: \"ping\"; token: string }\n | {\n op: \"exec\";\n token: string;\n /** Shell form — spawned as `sh -c command` (setup/start commands). */\n command?: string;\n /** Argv form — spawned directly, no shell (git and friends). */\n argv?: string[];\n cwd: string;\n env?: Record<string, string>;\n }\n | {\n op: \"pty\";\n token: string;\n file: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n cols: number;\n rows: number;\n }\n | { op: \"snapshotBuild\"; token: string; cwd: string }\n | { op: \"snapshotExtract\"; token: string; cwd: string }\n | { op: \"readFile\"; token: string; path: string }\n | { op: \"stat\"; token: string; path: string }\n | { op: \"readdir\"; token: string; path: string };\n\n/** Mid-stream frames (either direction after the opening request). */\nexport type WorkbenchFrame =\n | { t: \"out\"; s: \"stdout\" | \"stderr\"; d: string }\n | { t: \"exit\"; code: number | null; signal: string | null }\n | { t: \"data\"; d: string }\n | { t: \"end\" }\n | { t: \"input\"; d: string }\n | { t: \"resize\"; cols: number; rows: number }\n | { t: \"kill\"; sig?: string }\n | { t: \"signal\"; mode: \"term-group\" }\n | {\n t: \"meta\";\n sizeBytes: number;\n fileCount: number;\n deletionCount: number;\n capturedAt: number;\n }\n | { t: \"result\"; value: unknown }\n | {\n t: \"stat\";\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n }\n | { t: \"entries\"; names: string[] }\n | { t: \"pong\"; version: string }\n | { t: \"error\"; message: string; code?: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** Parse one NDJSON line into a frame-shaped record, or null when malformed. */\nexport function parseLine(line: string): Record<string, unknown> | null {\n if (!line) return null;\n try {\n const parsed: unknown = JSON.parse(line);\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Incremental NDJSON reader. Feed raw socket chunks; complete lines are\n * parsed and handed to the callback (malformed lines are dropped — the same\n * contract as the hook socket's envelope parsing).\n */\nexport class FrameReader {\n private buffer = \"\";\n\n constructor(private readonly onFrame: (frame: Record<string, unknown>) => void) {}\n\n push(chunk: Buffer | string): void {\n this.buffer += chunk.toString();\n let index = this.buffer.indexOf(\"\\n\");\n while (index >= 0) {\n const line = this.buffer.slice(0, index);\n this.buffer = this.buffer.slice(index + 1);\n const frame = parseLine(line);\n if (frame) this.onFrame(frame);\n index = this.buffer.indexOf(\"\\n\");\n }\n }\n}\n\n/** Serialize + write one frame. Write errors are the socket's problem —\n * callers handle teardown via the socket's own error/close events. */\nexport function writeFrame(\n socket: Pick<Socket, \"write\">,\n frame: WorkbenchRequest | WorkbenchFrame,\n): void {\n try {\n socket.write(`${JSON.stringify(frame)}\\n`);\n } catch {\n /* socket already destroyed — close handling owns cleanup */\n }\n}\n\nexport const DEFAULT_WORKBENCH_PORT = 7411;\n\n/** Matches the timingSafeEqualStr helper used for every other shared-secret\n * comparison in the codebase (preview-resolve.ts, workspace-attach-token.ts,\n * snapshot-link.ts) — constant-time so the loopback auth check can't leak\n * the token via response-time. */\nexport function timingSafeTokenEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return crypto.timingSafeEqual(ab, bb);\n}\n"],"mappings":";AAaA,OAAO,YAAY;AA8DnB,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAGO,SAAS,UAAU,MAA8C;AACtE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,WAAO,SAAS,MAAM,IAAI,SAAS;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,IAAM,cAAN,MAAkB;AAAA,EAGvB,YAA6B,SAAmD;AAAnD;AAAA,EAAoD;AAAA,EAApD;AAAA,EAFrB,SAAS;AAAA,EAIjB,KAAK,OAA8B;AACjC,SAAK,UAAU,MAAM,SAAS;AAC9B,QAAI,QAAQ,KAAK,OAAO,QAAQ,IAAI;AACpC,WAAO,SAAS,GAAG;AACjB,YAAM,OAAO,KAAK,OAAO,MAAM,GAAG,KAAK;AACvC,WAAK,SAAS,KAAK,OAAO,MAAM,QAAQ,CAAC;AACzC,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,MAAO,MAAK,QAAQ,KAAK;AAC7B,cAAQ,KAAK,OAAO,QAAQ,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAIO,SAAS,WACd,QACA,OACM;AACN,MAAI;AACF,WAAO,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AAAA,EAC3C,QAAQ;AAAA,EAER;AACF;AAEO,IAAM,yBAAyB;AAM/B,SAAS,qBAAqB,GAAW,GAAoB;AAClE,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,MAAI,GAAG,WAAW,GAAG,OAAQ,QAAO;AACpC,SAAO,OAAO,gBAAgB,IAAI,EAAE;AACtC;","names":[]}
|