MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / begin

Method begin

src/agent/git-sandbox.ts:140–217  ·  view source on GitHub ↗

* Enter the sandbox. Returns true if isolation is now active, false if it * couldn't be set up (non-git, detached HEAD, etc.) — in which case the caller * proceeds without isolation.

(taskId: string, signal?: AbortSignal)

Source from the content-addressed store, hash-verified

138 * proceeds without isolation.
139 */
140 async begin(taskId: string, signal?: AbortSignal): Promise<boolean> {
141 let lock: LockHandle | null = null;
142 try {
143 if (!(await isGitRepo(this.cwd, signal))) {
144 logger.debug('GitSandbox: not a git repo — running without isolation');
145 return false;
146 }
147 const repoRoot = await repoToplevel(this.cwd, signal);
148 if (!repoRoot) {
149 logger.debug('GitSandbox: could not resolve repo root — skipping isolation');
150 return false;
151 }
152
153 // Advisory repo lock: stop two sessions in one repo interleaving HEAD/stash
154 // mutations. A short retry budget means a LIVE holder makes us decline (run
155 // without isolation) rather than block; a dead holder's stale lock is
156 // reclaimed by acquireLock's staleMs path. The lock dir must exist first.
157 const lockPath = sandboxLockPath(repoRoot);
158 await fs.mkdir(path.dirname(lockPath), { recursive: true }).catch(() => {});
159 try {
160 lock = await acquireLock(lockPath, { retries: 3, intervalMs: 150, staleMs: 30_000 });
161 } catch {
162 logger.info('GitSandbox: another session holds the sandbox lock — running without isolation');
163 return false;
164 }
165
166 const origin = await this.currentBranch(signal);
167 if (!origin) {
168 logger.debug('GitSandbox: detached HEAD — skipping isolation');
169 await lock.release();
170 return false;
171 }
172 const base = await this.headSha(signal);
173 if (!base) { await lock.release(); return false; }
174
175 // Stash any uncommitted work so the sandbox starts from a clean tree and
176 // the user's WIP is preserved untouched. The message is unique per task so
177 // finish() can restore THIS stash by message, never another session's.
178 const status = await git(['status', '--porcelain'], { cwd: this.cwd, signal });
179 const dirty = (status.exitCode === 0) && status.stdout.trim().length > 0;
180 const stashMessage = `qodex-sandbox-wip-${taskId}`;
181 let stashed = false;
182 if (dirty) {
183 const s = await git(['stash', 'push', '-u', '-m', stashMessage], { cwd: this.cwd, signal });
184 stashed = (s.exitCode === 0);
185 if (!(s.exitCode === 0)) {
186 logger.debug('GitSandbox: stash failed — skipping isolation to avoid touching dirty tree');
187 await lock.release();
188 return false;
189 }
190 }
191
192 const branch = `qodex/sandbox-${taskId}`;
193 const co = await git(['checkout', '-b', branch], { cwd: this.cwd, signal });
194 if (!(co.exitCode === 0)) {
195 // Roll back the stash if we made one, then bail.
196 if (stashed) await this.restoreStashByMessage(stashMessage, signal);
197 logger.debug('GitSandbox: branch create failed — no isolation', { err: co.stderr });

Callers 5

runSandboxedMethod · 0.95
makeServerToolContextFunction · 0.45
runMethod · 0.45

Calls 11

currentBranchMethod · 0.95
headShaMethod · 0.95
restoreStashByMessageMethod · 0.95
writeRecoveryRecordMethod · 0.95
isGitRepoFunction · 0.85
repoToplevelFunction · 0.85
sandboxLockPathFunction · 0.85
acquireLockFunction · 0.85
debugMethod · 0.80
infoMethod · 0.80
gitFunction · 0.50

Tested by

no test coverage detected