22 * AssetPack Pipeline - Clone VCS Repository Agent (Setup).
33 *
44 * Pipeline executions always run on a **Host** (LocalHost, VercelSandboxHost, …).
5- * Cloning is a Setup responsibility only — never pre-pipeline initialization.
5+ * Cloning is a Setup responsibility only — never pre-pipeline initialization,
6+ * never inside a serverless function process, and not via Sandbox.create git
7+ * source (that is outside the pipeline).
68 *
79 * For this pipeline run the agent ensures a **complete working tree at the
810 * requested SHA/ref** (all files; shallow history is fine):
9- * 1. If the Host already has the repository for this run (e.g. VercelSandboxHost
10- * provisioned the git source on the Host image), adopt that checkout.
11- * 2. Else if the Host wired `deposit:cloneRepositoryForRun` (LocalHost deposit),
11+ * 1. If the Host wired `deposit:cloneRepositoryForRun` (LocalHost deposit),
1212 * clone into an ephemeral Host workspace for this run only.
13- * 3. Else clone via the Setup VCS clone tool on the Host.
13+ * 2. Else if `BITCODE_HOST_CLONE_*` env is set (sandbox deposit: create with
14+ * image only + clone specs in env), multi-step git clone **inside the box**.
15+ * 3. Else if the Host already has a real checkout for this run (`.git` present
16+ * or explicit workspace path), adopt that tree.
17+ * 4. Else clone via the Setup VCS clone tool on the Host.
1418 *
1519 * LocalHost never reads paths outside the workspace cloned for this run.
1620 * Discovery builds the source catalog from that same tree (not a second clone).
1721 */
1822
23+ import { execFile } from 'node:child_process' ;
24+ import { existsSync , statSync , promises as fs } from 'node:fs' ;
25+ import os from 'node:os' ;
26+ import path from 'node:path' ;
27+
1928import { factoryPTRRAgent } from '@bitcode/agent-generics' ;
2029import { Prompt } from '@bitcode/prompts/prompt' ;
2130import {
@@ -25,6 +34,11 @@ import {
2534 VCS_REFINE_PROMPT ,
2635 VCS_RETRY_PROMPT ,
2736} from '@bitcode/generic-agents-vcs' ;
37+ import {
38+ provisionGitWorkingTree ,
39+ readHostCloneEnv ,
40+ type HostExec ,
41+ } from '@bitcode/host-generics' ;
2842import {
2943 DP_CLONE_VCS_SYSTEM_PROMPT ,
3044 DP_CLONE_VCS_PLAN_PROMPT ,
@@ -162,8 +176,9 @@ function normalizeRepositoryInput(input: any, execution: any): {
162176}
163177
164178/**
165- * True when the Host already has this run's repository working tree available
166- * (VercelSandboxHost image source, explicit Host workspace path, etc.).
179+ * True when the Host already has this run's repository working tree available.
180+ * Must be a real checkout — never treat bare `sourceRevision` metadata as proof
181+ * the customer tree is on disk (that falsely adopted empty sandbox CWD).
167182 */
168183function resolveHostAvailableWorkspace (
169184 input : any ,
@@ -179,26 +194,128 @@ function resolveHostAvailableWorkspace(
179194 return { workspacePath : explicitPath , availability : 'host-workspace-path' } ;
180195 }
181196
182- // Host plan already provisioned git source on the Host image for this run
183- // (VercelSandboxHost and similar). sourceRevision is stored on the Host context.
184- const hostSourceRevision = resolveHostSourceRevision ( input , execution ) ;
185- const hostContext =
186- input ?. host ??
187- findExecutionValue ( execution , 'host' , 'manifest' ) ??
188- findExecutionValue ( execution , 'host' , 'hostKind' ) ;
189- if ( hostSourceRevision && hostContext != null ) {
190- // Working tree root on the Host image for this pipeline process.
191- return { workspacePath : process . cwd ( ) , availability : 'host-image-source' } ;
192- }
193- // sourceRevision alone (from Host runner input) without LocalHost cloner:
194- // Host already placed the tree for in-box execution.
195- if ( hostSourceRevision && ! findExecutionValue ( execution , 'deposit' , DEPOSIT_CLONE_REPOSITORY_FOR_RUN_KEY ) ) {
196- return { workspacePath : process . cwd ( ) , availability : 'host-image-source' } ;
197+ // Legacy create-time git source: only adopt CWD when a real .git exists.
198+ // (New deposit path uses BITCODE_HOST_CLONE_* + Setup multi-step clone.)
199+ const cwd = process . cwd ( ) ;
200+ if ( existsSync ( path . join ( cwd , '.git' ) ) ) {
201+ return { workspacePath : cwd , availability : 'host-cwd-git-present' } ;
197202 }
198203
199204 return null ;
200205}
201206
207+ function defaultSetupHostExec ( ) : HostExec {
208+ return ( cmd , args , opts ) =>
209+ new Promise ( ( resolve ) => {
210+ execFile (
211+ cmd ,
212+ args ,
213+ {
214+ cwd : opts ?. cwd ,
215+ env : opts ?. env ? { ...process . env , ...opts . env } : process . env ,
216+ maxBuffer : 64 * 1024 * 1024 ,
217+ } ,
218+ ( error , stdout , stderr ) => {
219+ const code =
220+ error && typeof ( error as NodeJS . ErrnoException & { code ?: unknown } ) . code === 'number'
221+ ? ( error as unknown as { code : number } ) . code
222+ : error
223+ ? 1
224+ : 0 ;
225+ resolve ( {
226+ exitCode : code ,
227+ stdout : stdout ?. toString ( ) ?? '' ,
228+ stderr : stderr ?. toString ( ) ?? '' ,
229+ } ) ;
230+ } ,
231+ ) ;
232+ } ) ;
233+ }
234+
235+ function slugRepo ( value : string ) : string {
236+ return value . replace ( / [ ^ a - z A - Z 0 - 9 . _ - ] + / g, '-' ) . replace ( / ^ - + | - + $ / g, '' ) . slice ( 0 , 80 ) || 'repo' ;
237+ }
238+
239+ /**
240+ * Setup in-box clone using BITCODE_HOST_CLONE_* env (sandbox deposit law path).
241+ * Multi-step git — not Vercel Sandbox.create source.
242+ */
243+ async function cloneFromHostEnvForRun ( execution : any ) : Promise < {
244+ workspacePath : string ;
245+ listFiles : ( ) => Promise < string [ ] > ;
246+ readFile : ( p : string ) => Promise < string | null > ;
247+ strategy : string ;
248+ } | null > {
249+ const spec = readHostCloneEnv ( ) ;
250+ if ( ! spec ) return null ;
251+
252+ const repoName = spec . repositoryFullName || 'customer/repo' ;
253+ const root =
254+ spec . root ||
255+ process . env . BITCODE_PIPELINE_HOST_ARTIFACT_DIR ?. replace ( / \/ \. b i t c o d e \/ p i p e l i n e - h o s t \/ ? $ / , '' ) ||
256+ ( existsDir ( '/vercel/sandbox' ) ? '/vercel/sandbox' : os . tmpdir ( ) ) ;
257+ const workspacePath = path . join (
258+ root ,
259+ `customer-source-${ slugRepo ( repoName ) } -${ Date . now ( ) . toString ( 36 ) } ` ,
260+ ) ;
261+
262+ log ( '[setup/clone] in-box host-env clone starting' , 'info' , {
263+ repository : repoName ,
264+ branch : spec . branch ,
265+ commit : spec . commit ? `${ spec . commit . slice ( 0 , 12 ) } …` : null ,
266+ hasToken : Boolean ( spec . password ) ,
267+ root,
268+ } ) ;
269+
270+ const exec = defaultSetupHostExec ( ) ;
271+ const result = await provisionGitWorkingTree ( {
272+ url : spec . url ,
273+ username : spec . username ,
274+ password : spec . password ,
275+ branch : spec . branch ,
276+ commit : spec . commit ,
277+ revision : spec . commit || spec . branch || 'HEAD' ,
278+ workspacePath,
279+ exec,
280+ } ) ;
281+
282+ log ( '[setup/clone] in-box host-env clone complete' , 'info' , {
283+ strategy : result . strategy ,
284+ workspacePath : result . workspacePath ,
285+ } ) ;
286+
287+ return {
288+ workspacePath : result . workspacePath ,
289+ strategy : result . strategy ,
290+ listFiles : async ( ) => {
291+ const listed = await exec ( 'git' , [ '-C' , result . workspacePath , 'ls-files' ] ) ;
292+ if ( listed . exitCode !== 0 ) return [ ] ;
293+ return listed . stdout
294+ . split ( / \r ? \n / )
295+ . map ( ( line ) => line . trim ( ) )
296+ . filter ( Boolean ) ;
297+ } ,
298+ readFile : async ( relativePath : string ) => {
299+ const absRoot = path . resolve ( result . workspacePath ) ;
300+ const absolute = path . resolve ( result . workspacePath , relativePath ) ;
301+ if ( absolute !== absRoot && ! absolute . startsWith ( absRoot + path . sep ) ) return null ;
302+ try {
303+ return await fs . readFile ( absolute , 'utf8' ) ;
304+ } catch {
305+ return null ;
306+ }
307+ } ,
308+ } ;
309+ }
310+
311+ function existsDir ( dirPath : string ) : boolean {
312+ try {
313+ return existsSync ( dirPath ) && statSync ( dirPath ) . isDirectory ( ) ;
314+ } catch {
315+ return false ;
316+ }
317+ }
318+
202319function presentCheckoutResult (
203320 normalized : NonNullable < ReturnType < typeof normalizeRepositoryInput > > ,
204321 workspacePath : string ,
@@ -351,27 +468,44 @@ export default async function runAssetPackCloneVCSRepositoryAgent(input: any, ex
351468 'localhost-clone-for-run' ,
352469 ) ;
353470 } else if ( normalized ) {
354- // 2) Host already has the repository for this run (e.g. VercelSandboxHost image).
355- const hostAvailable = resolveHostAvailableWorkspace ( input , execution ) ;
356- if ( hostAvailable ) {
471+ // 2) Sandbox deposit: clone customer repo inside the box from host env
472+ // (Sandbox.create had image only — no create-time git source).
473+ const hostEnvClone = await cloneFromHostEnvForRun ( execution ) ;
474+ if ( hostEnvClone ) {
475+ await recordDepositCatalogFromRunWorkspace ( execution , hostEnvClone ) ;
357476 out = presentCheckoutResult (
358477 normalized ,
359- hostAvailable . workspacePath ,
360- 'host-source-present ' ,
361- hostAvailable . availability ,
478+ hostEnvClone . workspacePath ,
479+ 'cloned-for-run ' ,
480+ `setup-in-box- ${ hostEnvClone . strategy } ` ,
362481 ) ;
363482 } else {
364- // 3) Clone on the Host via Setup VCS tool.
365- out = await AssetPackCloneVCSRepositoryAgent ( input , execution ) ;
366- if ( out && typeof out === 'object' ) {
367- out = {
368- ...out ,
369- metadata : {
370- ...( out . metadata || { } ) ,
371- workingTree : 'complete-at-revision' ,
372- hostProvision : 'setup-clone-on-host' ,
373- } ,
374- } ;
483+ // 3) Host already has a real checkout for this run.
484+ const hostAvailable = resolveHostAvailableWorkspace ( input , execution ) ;
485+ if ( hostAvailable ) {
486+ out = presentCheckoutResult (
487+ normalized ,
488+ hostAvailable . workspacePath ,
489+ 'host-source-present' ,
490+ hostAvailable . availability ,
491+ ) ;
492+ } else {
493+ // 4) Clone on the Host via Setup VCS tool.
494+ log ( '[setup/clone] falling back to Setup VCS clone tool' , 'info' , {
495+ hasNormalized : Boolean ( normalized ) ,
496+ hasHostCloneEnv : Boolean ( readHostCloneEnv ( ) ) ,
497+ } ) ;
498+ out = await AssetPackCloneVCSRepositoryAgent ( input , execution ) ;
499+ if ( out && typeof out === 'object' ) {
500+ out = {
501+ ...out ,
502+ metadata : {
503+ ...( out . metadata || { } ) ,
504+ workingTree : 'complete-at-revision' ,
505+ hostProvision : 'setup-clone-on-host' ,
506+ } ,
507+ } ;
508+ }
375509 }
376510 }
377511 } else {
0 commit comments