Skip to content

Commit 0c78f3e

Browse files
V48 (impl-only): Host law — Setup in-box clone, no Sandbox.create git source
Deposit on serverless creates the Pipeliner image only (no create-time customer git source). Clone specs pass via BITCODE_HOST_CLONE_* env; Setup multi-step clones inside the box (branch shallow + pin commit). Shared provisionGitWorkingTree backs LocalHost and Setup. Telemetry: cloneLocation=setup-in-box. Requires Pipeliner image rebuild for Setup agent package changes.
1 parent c598234 commit 0c78f3e

12 files changed

Lines changed: 681 additions & 240 deletions

File tree

packages/asset-packs-pipelines/domain/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"@bitcode/generic-measurements-absolutes": "workspace:*",
7070
"@bitcode/generic-measurements-needinesses": "workspace:*",
7171
"@bitcode/asset-packs-generics": "workspace:*",
72-
"@bitcode/generic-asset-packs-synthesis": "workspace:*"
72+
"@bitcode/generic-asset-packs-synthesis": "workspace:*",
73+
"@bitcode/host-generics": "workspace:*"
7374
},
7475
"devDependencies": {
7576
"@types/node": "^20.0.0",

packages/asset-packs-pipelines/domain/src/agents/setup/asset-pack-clone-vcs-repository-agent.ts

Lines changed: 173 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
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+
1928
import { factoryPTRRAgent } from '@bitcode/agent-generics';
2029
import { Prompt } from '@bitcode/prompts/prompt';
2130
import {
@@ -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';
2842
import {
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
*/
168183
function 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-zA-Z0-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(/\/\.bitcode\/pipeline-host\/?$/, '') ||
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+
202319
function 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 {

packages/generic-hosts/Local/src/__tests__/local-host.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ describe('LocalHost (primitive Host implementation)', () => {
8686
await expect(fs.stat(ws.workspacePath)).rejects.toBeDefined(); // removed
8787
});
8888

89+
it('prefers branch --branch for shallow clone when branch is provided', async () => {
90+
const { exec, calls } = fakeExec();
91+
const host = new LocalHost({ exec, rootDir: await makeRoot() });
92+
await host.provisionRepository({
93+
repositoryFullName: 'o/r',
94+
url: 'https://github.com/o/r.git',
95+
revision: 'abc1234',
96+
branch: 'version/v48',
97+
commit: 'abc1234',
98+
});
99+
const cloneCall = calls.find((c) => c[0] === 'git' && c[1] === 'clone');
100+
expect(cloneCall).toEqual(expect.arrayContaining(['--branch', 'version/v48']));
101+
});
102+
89103
it('readFile refuses path traversal outside the checkout', async () => {
90104
const host = new LocalHost({ exec: fakeExec().exec, rootDir: await makeRoot() });
91105
const ws = await host.provisionRepository({
@@ -131,7 +145,7 @@ describe('LocalHost (primitive Host implementation)', () => {
131145
revision: 'main',
132146
password: 'ghs_secrettoken',
133147
}),
134-
).rejects.toThrow(/LocalHost git clone failed/);
148+
).rejects.toThrow(/Host git clone failed/);
135149

136150
const cloneCall = calls.find((c) => c[1] === 'clone')!;
137151
// cloneArgs: clone --depth 1 --single-branch [--branch rev] <url> <dest>

0 commit comments

Comments
 (0)