pi-codex-marketplace 0.1.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/extensions/pi/git-registration.ts +138 -0
  4. package/extensions/pi/index.ts +293 -0
  5. package/extensions/pi/installation.ts +90 -0
  6. package/extensions/pi/journal.ts +80 -0
  7. package/extensions/pi/lifecycle.ts +285 -0
  8. package/extensions/pi/registration.ts +143 -0
  9. package/extensions/pi/scope-overrides.ts +170 -0
  10. package/package.json +60 -0
  11. package/src/barrier/global-barrier.ts +105 -0
  12. package/src/bridge-state/atomic.ts +237 -0
  13. package/src/bridge-state/index.ts +5 -0
  14. package/src/bridge-state/migrate.ts +261 -0
  15. package/src/bridge-state/paths.ts +75 -0
  16. package/src/bridge-state/repair.ts +185 -0
  17. package/src/bridge-state/schema.ts +70 -0
  18. package/src/bridge-state/store.ts +489 -0
  19. package/src/bridge-state/types.ts +170 -0
  20. package/src/cache/index.ts +2 -0
  21. package/src/cache/paths.ts +42 -0
  22. package/src/cache/source-cache.ts +365 -0
  23. package/src/compatibility/index.ts +1 -0
  24. package/src/compatibility/profile.ts +328 -0
  25. package/src/installation/flow.ts +443 -0
  26. package/src/installation/index.ts +1 -0
  27. package/src/installation/inspection.ts +129 -0
  28. package/src/journal/active-chains.ts +99 -0
  29. package/src/journal/index.ts +3 -0
  30. package/src/journal/journal.ts +215 -0
  31. package/src/journal/types.ts +49 -0
  32. package/src/lifecycle/index.ts +5 -0
  33. package/src/lifecycle/rebind.ts +290 -0
  34. package/src/lifecycle/refresh.ts +407 -0
  35. package/src/lifecycle/removal.ts +457 -0
  36. package/src/lifecycle/update-plan.ts +222 -0
  37. package/src/lifecycle/update.ts +303 -0
  38. package/src/projection/collision.ts +120 -0
  39. package/src/projection/effective-state.ts +182 -0
  40. package/src/projection/index.ts +4 -0
  41. package/src/projection/overrides.ts +230 -0
  42. package/src/projection/project.ts +359 -0
  43. package/src/reconciliation/startup.ts +144 -0
  44. package/src/registration/budget.ts +28 -0
  45. package/src/registration/catalog.ts +224 -0
  46. package/src/registration/contained.ts +140 -0
  47. package/src/registration/fence.ts +86 -0
  48. package/src/registration/findings.ts +188 -0
  49. package/src/registration/flow.ts +619 -0
  50. package/src/registration/git-acquisition.ts +481 -0
  51. package/src/registration/git-flow.ts +654 -0
  52. package/src/registration/git-locator.ts +380 -0
  53. package/src/registration/git-selector.ts +279 -0
  54. package/src/registration/index.ts +16 -0
  55. package/src/registration/receipt.ts +305 -0
  56. package/src/registration/registration.ts +102 -0
  57. package/src/registration/snapshot.ts +382 -0
  58. package/src/registration/source-key.ts +111 -0
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Git Source Acquisition — non-executing retrieval of a Git Marketplace Source at a Resolved Revision.
3
+ * See CONTEXT.md: Source Acquisition, Acquisition Trust Base.
4
+ *
5
+ * Guarantees:
6
+ * - Never runs repository-controlled hooks, filters, submodules, dependencies, or Plugin components.
7
+ * - Only trusts selected Git/SSH, system CA, existing known-hosts, approved credential helper/agent.
8
+ * - Rejects unknown/changed SSH host keys and canonical-locator-changing redirects (Blocking Findings).
9
+ * - Rejects extends trust to repository content or repo-controlled git config.
10
+ *
11
+ * Implementation uses `git clone --no-checkout` with hardened config and environment, plus
12
+ * `git ls-remote` for Resolved Revision binding before checkout.
13
+ */
14
+
15
+ import { spawn } from 'node:child_process';
16
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import type { Scope } from '../bridge-state/types.js';
21
+ import { CODE, RULE, blocking, type ValidationFinding } from './findings.js';
22
+ import type { CanonicalGitLocator } from './git-locator.js';
23
+ import type { NormalizedGitSelector } from './git-selector.js';
24
+
25
+ export interface GitExecutor {
26
+ (args: string[], opts?: { cwd?: string; env?: Record<string, string> }): Promise<{
27
+ exitCode: number;
28
+ stdout: string;
29
+ stderr: string;
30
+ }>;
31
+ }
32
+
33
+ /** Default executor that spawns `git` */
34
+ export function defaultGitExecutor(): GitExecutor {
35
+ return (args, opts) =>
36
+ new Promise((resolve) => {
37
+ const env = { ...process.env, ...opts?.env } as Record<string, string>;
38
+ const child = spawn('git', args, {
39
+ cwd: opts?.cwd,
40
+ env,
41
+ stdio: ['ignore', 'pipe', 'pipe'],
42
+ });
43
+ let stdout = '';
44
+ let stderr = '';
45
+ child.stdout?.on('data', (d) => (stdout += String(d)));
46
+ child.stderr?.on('data', (d) => (stderr += String(d)));
47
+ child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
48
+ child.on('error', (err) => resolve({ exitCode: 1, stdout: '', stderr: String(err) }));
49
+ });
50
+ }
51
+
52
+ export interface AcquisitionTrustOptions {
53
+ /** Path to known_hosts file for SSH host key verification; defaults to ~/.ssh/known_hosts */
54
+ knownHostsFile?: string;
55
+ /** Allowed credential helpers (empty means none approved). If undefined, we disable all. */
56
+ allowedCredentialHelpers?: string[];
57
+ /** Selected git binary path (for provenance) */
58
+ gitPath?: string;
59
+ /** Selected ssh command (for provenance) */
60
+ sshCommand?: string;
61
+ /** Allow redirect that preserves canonical locator host? false = reject host-changing redirects */
62
+ allowRedirects?: boolean;
63
+ }
64
+
65
+ export interface AcquireOptions {
66
+ scope: Scope;
67
+ /** For isolated test dirs */
68
+ cwd?: string;
69
+ agentDir?: string;
70
+ locator: CanonicalGitLocator;
71
+ selector: NormalizedGitSelector;
72
+ trust?: AcquisitionTrustOptions;
73
+ /** Injected git executor for tests */
74
+ executor?: GitExecutor;
75
+ /** Destination directory (if not provided, a temp dir is created) */
76
+ destDir?: string;
77
+ /** Timeout for git operations (ms) */
78
+ timeoutMs?: number;
79
+ }
80
+
81
+ export interface AcquireResult {
82
+ ok: boolean;
83
+ /** Directory containing the acquired repo (checkout of resolved revision) */
84
+ acquiredPath?: string;
85
+ /** Full 40/64 hex Resolved Revision bound before confirmation */
86
+ resolvedRevision?: string;
87
+ findings: ValidationFinding[];
88
+ /** Raw executor stderr for diagnostics */
89
+ stderr?: string;
90
+ /** Whether the destDir should be cleaned up by caller (true when we created it) */
91
+ createdTemp?: boolean;
92
+ }
93
+
94
+ function trustFinding(scope: Scope, code: string, rule: string, outcome: string): ValidationFinding {
95
+ return blocking({
96
+ code,
97
+ phase: 'validation',
98
+ target: 'source',
99
+ scope,
100
+ pointer: '',
101
+ rule,
102
+ outcome,
103
+ });
104
+ }
105
+
106
+ function acquireFinding(scope: Scope, outcome: string): ValidationFinding {
107
+ return trustFinding(scope, CODE.GIT_ACQUISITION_FAILED, RULE.GIT_ACQUISITION_FAILED, outcome);
108
+ }
109
+
110
+ /** Build hardened git env and config for non-executing acquisition */
111
+ function hardenedEnv(trust: AcquisitionTrustOptions | undefined, locator: CanonicalGitLocator): Record<string, string> {
112
+ const env: Record<string, string> = {
113
+ GIT_TERMINAL_PROMPT: '0',
114
+ GIT_ASKPASS: 'echo',
115
+ SSH_ASKPASS: 'echo',
116
+ // Disable LFS smudge by default to avoid executing filters
117
+ GIT_LFS_SKIP_SMUDGE: '1',
118
+ };
119
+ // SSH trust: StrictHostKeyChecking=yes, known_hostsFile must exist; BatchMode=yes prevents interactive.
120
+ // If locator is ssh, we set GIT_SSH_COMMAND appropriately.
121
+ if (locator.transport === 'ssh') {
122
+ const knownHosts = trust?.knownHostsFile ?? join(process.env.HOME ?? tmpdir(), '.ssh', 'known_hosts');
123
+ // Only trust pre-established known_hosts; we do not create it.
124
+ let sshCmd = trust?.sshCommand ?? 'ssh';
125
+ sshCmd += ` -o StrictHostKeyChecking=yes -o BatchMode=yes -o UserKnownHostsFile="${knownHosts.replace(/"/g, '\\"')}"`;
126
+ // Also disable adding keys automatically
127
+ sshCmd += ' -o CheckHostIP=yes';
128
+ env.GIT_SSH_COMMAND = sshCmd;
129
+ }
130
+ // Credential helpers: by default disable all (empty credential.helper). Approved helpers could be allowed via config,
131
+ // but per spec we only permit necessary trust + approved helper/agent. For scaffold we disable unless explicitly allowed.
132
+ // This is handled via -c credential.helper= config, not env.
133
+ return env;
134
+ }
135
+
136
+ function hardenedConfigArgs(trust?: AcquisitionTrustOptions): string[] {
137
+ const args: string[] = [];
138
+ // Disable hooks: point hooksPath to /dev/null
139
+ args.push('-c', 'core.hooksPath=/dev/null');
140
+ // Disable credential helpers unless approved
141
+ if (!trust?.allowedCredentialHelpers || trust.allowedCredentialHelpers.length === 0) {
142
+ args.push('-c', 'credential.helper=');
143
+ } else {
144
+ // allow only specified helpers; first clear then add allowed
145
+ args.push('-c', 'credential.helper=');
146
+ for (const h of trust.allowedCredentialHelpers) {
147
+ args.push('-c', `credential.helper=${h}`);
148
+ }
149
+ }
150
+ // Never follow redirects that would change canonical locator host — disable http redirects by default
151
+ // If allowRedirects is explicitly true, we skip this and allow git default (follow). Otherwise block.
152
+ if (trust?.allowRedirects !== true) {
153
+ args.push('-c', 'http.followRedirects=false');
154
+ }
155
+ // Ensure SSL verification uses system CA (default)
156
+ args.push('-c', 'http.sslVerify=true');
157
+ // Disable filter process execution
158
+ args.push('-c', 'filter.lfs.process=');
159
+ args.push('-c', 'filter.lfs.required=false');
160
+ return args;
161
+ }
162
+
163
+ function isFullHex(s: string): boolean {
164
+ return /^[0-9a-f]{40}$/.test(s) || /^[0-9a-f]{64}$/.test(s);
165
+ }
166
+
167
+ /** Resolve a selector to a full commit SHA via `git ls-remote` (except commit selector which is already resolved) */
168
+ async function resolveRevision(
169
+ locator: CanonicalGitLocator,
170
+ selector: NormalizedGitSelector,
171
+ scope: Scope,
172
+ executor: GitExecutor,
173
+ env: Record<string, string>,
174
+ configArgs: string[],
175
+ ): Promise<{ ok: true; sha: string } | { ok: false; findings: ValidationFinding[]; stderr?: string }> {
176
+ // commit selector: already full hex, canonical is the sha (lowercased)
177
+ if (selector.kind === 'commit') {
178
+ const sha = selector.canonical;
179
+ if (!isFullHex(sha)) {
180
+ return {
181
+ ok: false,
182
+ findings: [
183
+ trustFinding(scope, CODE.GIT_RESOLVED_REVISION_INVALID, RULE.GIT_RESOLVED_REVISION_INVALID, `commit selector resolved revision is not full hex: '${sha}'`),
184
+ ],
185
+ };
186
+ }
187
+ // Verify that the commit is reachable (try ls-remote for that sha — many servers support, but if not,
188
+ // we will verify after clone via cat-file). For now, assume reachable if format is valid; clone step will verify.
189
+ return { ok: true, sha };
190
+ }
191
+
192
+ // default => HEAD ; branch => refs/heads/* ; tag => refs/tags/*
193
+ let remoteRef: string;
194
+ if (selector.kind === 'default') {
195
+ remoteRef = 'HEAD';
196
+ } else {
197
+ remoteRef = selector.canonical; // already refs/heads/... or refs/tags/...
198
+ }
199
+
200
+ const lsArgs = [...configArgs, 'ls-remote', locator.canonicalUrl, remoteRef];
201
+ const res = await executor(lsArgs, { env });
202
+ if (res.exitCode !== 0) {
203
+ // Distinguish trust failures from generic acquisition failures via stderr
204
+ const stderr = (res.stderr || '').toLowerCase();
205
+ if (stderr.includes('host key verification failed') || stderr.includes('unknown host key') || stderr.includes('offending')) {
206
+ const isChanged = stderr.includes('changed') || stderr.includes('offending') || stderr.includes('key changed');
207
+ const code = isChanged ? CODE.GIT_TRUST_HOST_KEY_CHANGED : CODE.GIT_TRUST_HOST_KEY_UNKNOWN;
208
+ return {
209
+ ok: false,
210
+ findings: [
211
+ trustFinding(
212
+ scope,
213
+ code,
214
+ RULE.GIT_TRUST_HOST_KEY,
215
+ `Acquisition Trust Base violation: SSH host key ${isChanged ? 'changed' : 'unknown'} for ${locator.host} — ${res.stderr.trim()} (only pre-established known-host keys are trusted)`,
216
+ ),
217
+ ],
218
+ stderr: res.stderr,
219
+ };
220
+ }
221
+ if (stderr.includes('redirect') || stderr.includes('moved') || stderr.includes('followredirects')) {
222
+ return {
223
+ ok: false,
224
+ findings: [
225
+ trustFinding(scope, CODE.GIT_TRUST_REDIRECT, RULE.GIT_TRUST_REDIRECT, `Acquisition Trust Base violation: redirect that would change canonical locator (followRedirects disabled) — ${res.stderr.trim()}`),
226
+ ],
227
+ stderr: res.stderr,
228
+ };
229
+ }
230
+ if (stderr.includes('could not read username') || stderr.includes('authentication failed') || stderr.includes('credential')) {
231
+ return {
232
+ ok: false,
233
+ findings: [
234
+ trustFinding(
235
+ scope,
236
+ CODE.GIT_ACQUISITION_FAILED,
237
+ RULE.GIT_TRUST_CREDENTIAL_HELPER,
238
+ `Acquisition Trust Base: credential helper/agent not approved — ${res.stderr.trim()}`,
239
+ ),
240
+ ],
241
+ stderr: res.stderr,
242
+ };
243
+ }
244
+ return {
245
+ ok: false,
246
+ findings: [acquireFinding(scope, `failed to resolve ${remoteRef} via ls-remote: ${res.stderr.trim() || `exit ${res.exitCode}`}`)],
247
+ stderr: res.stderr,
248
+ };
249
+ }
250
+
251
+ const out = res.stdout.trim();
252
+ if (!out) {
253
+ return {
254
+ ok: false,
255
+ findings: [acquireFinding(scope, `ls-remote returned no match for ${remoteRef} at ${locator.canonicalUrl}`)],
256
+ stderr: res.stderr,
257
+ };
258
+ }
259
+
260
+ // ls-remote output: "<sha>\t<ref>" per line. For HEAD, may also include symref info when using --symref, but we used plain.
261
+ // For default HEAD, we want the sha of HEAD. For branch/tag, we get sha of that ref. For annotated tags, server may return both refs/tags/v1 and refs/tags/v1^{}.
262
+ // Prefer the peeled commit line (suffix ^{}) when present, otherwise fall back to the first line, ensuring Resolved Revision is always the commit.
263
+ const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
264
+ const peeledLine = lines.find((l) => l.includes('^{}'));
265
+ const targetLine = peeledLine ?? lines[0];
266
+ const tabIdx = targetLine.indexOf('\t');
267
+ const sha = tabIdx >= 0 ? targetLine.slice(0, tabIdx).trim() : targetLine.split(/\s+/)[0].trim();
268
+ if (!isFullHex(sha.toLowerCase())) {
269
+ return {
270
+ ok: false,
271
+ findings: [trustFinding(scope, CODE.GIT_RESOLVED_REVISION_INVALID, RULE.GIT_RESOLVED_REVISION_INVALID, `resolved revision is not full hex: '${sha}'`)],
272
+ stderr: res.stderr,
273
+ };
274
+ }
275
+ return { ok: true, sha: sha.toLowerCase() };
276
+ }
277
+
278
+ /** Resolve a selector to a full commit SHA via non-executing ls-remote (#22 cache seam). */
279
+ export async function resolveGitRevision(
280
+ locator: CanonicalGitLocator,
281
+ selector: NormalizedGitSelector,
282
+ scope: Scope,
283
+ opts: { executor?: GitExecutor; trust?: AcquisitionTrustOptions } = {},
284
+ ): Promise<{ ok: true; sha: string } | { ok: false; findings: ValidationFinding[]; stderr?: string }> {
285
+ const env = hardenedEnv(opts.trust, locator);
286
+ const configArgs = hardenedConfigArgs(opts.trust);
287
+ return resolveRevision(locator, selector, scope, opts.executor ?? defaultGitExecutor(), env, configArgs);
288
+ }
289
+
290
+ /**
291
+ * Acquire a Git source non-executingly at its Resolved Revision.
292
+ * Uses `clone --no-checkout` with hardened config/env, then checks out the resolved revision.
293
+ * Caller is responsible for cleaning the acquiredPath when done (if createdTemp is true) after validation.
294
+ */
295
+ export async function acquireGitSource(opts: AcquireOptions): Promise<AcquireResult> {
296
+ const scope = opts.scope;
297
+ const locator = opts.locator;
298
+ const selector = opts.selector;
299
+ const executor = opts.executor ?? defaultGitExecutor();
300
+ const trust = opts.trust;
301
+ const env = hardenedEnv(trust, locator);
302
+ const configArgs = hardenedConfigArgs(trust);
303
+
304
+ // Resolve revision first (before clone) — binds full commit
305
+ const resolved = await resolveRevision(locator, selector, scope, executor, env, configArgs);
306
+ if (!resolved.ok) {
307
+ return { ok: false, findings: (resolved as { findings: ValidationFinding[] }).findings, stderr: (resolved as { stderr?: string }).stderr };
308
+ }
309
+ const sha = (resolved as { sha: string }).sha;
310
+
311
+ // Prepare destination
312
+ let dest: string;
313
+ let createdTemp = false;
314
+ if (opts.destDir) {
315
+ dest = opts.destDir;
316
+ } else {
317
+ dest = mkdtempSync(join(tmpdir(), 'git-acq-'));
318
+ createdTemp = true;
319
+ }
320
+
321
+ // Clone --no-checkout (non-executing: hooks/filters disabled via config)
322
+ // Note: order is git [configArgs] clone --no-checkout --filter=blob:none <url> <dest>
323
+ // We use --filter=blob:none to reduce bytes but not essential; we include it as non-executing hint.
324
+ const cloneArgs = [...configArgs, 'clone', '--no-checkout', '--filter=blob:none', locator.canonicalUrl, dest];
325
+ const cloneRes = await executor(cloneArgs, { env });
326
+ if (cloneRes.exitCode !== 0) {
327
+ const stderr = cloneRes.stderr || '';
328
+ const lower = stderr.toLowerCase();
329
+ if (lower.includes('host key verification failed') || lower.includes('unknown host key') || lower.includes('offending')) {
330
+ const isChanged = lower.includes('changed') || lower.includes('offending');
331
+ const code = isChanged ? CODE.GIT_TRUST_HOST_KEY_CHANGED : CODE.GIT_TRUST_HOST_KEY_UNKNOWN;
332
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
333
+ return {
334
+ ok: false,
335
+ findings: [
336
+ trustFinding(
337
+ scope,
338
+ code,
339
+ RULE.GIT_TRUST_HOST_KEY,
340
+ `Acquisition Trust Base violation: SSH host key ${isChanged ? 'changed' : 'unknown'} — ${stderr.trim()}`,
341
+ ),
342
+ ],
343
+ stderr,
344
+ };
345
+ }
346
+ if (lower.includes('redirect') || lower.includes('moved permanently') || lower.includes('followredirects')) {
347
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
348
+ return {
349
+ ok: false,
350
+ findings: [
351
+ trustFinding(scope, CODE.GIT_TRUST_REDIRECT, RULE.GIT_TRUST_REDIRECT, `Acquisition Trust Base violation: redirect changing canonical locator — ${stderr.trim()}`),
352
+ ],
353
+ stderr,
354
+ };
355
+ }
356
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
357
+ return {
358
+ ok: false,
359
+ findings: [acquireFinding(scope, `git clone failed: ${stderr.trim() || `exit ${cloneRes.exitCode}`}`)],
360
+ stderr,
361
+ };
362
+ }
363
+
364
+ // Verify trust: redirect that changes host — check remote origin URL after clone
365
+ // If http.followRedirects was disabled, clone would have failed above. If redirects were allowed but changed host, we detect.
366
+ // We fetch the stored origin URL and compare host.
367
+ if (trust?.allowRedirects !== true) {
368
+ const remoteRes = await executor([...configArgs, '-C', dest, 'remote', 'get-url', 'origin'], { env });
369
+ if (remoteRes.exitCode === 0) {
370
+ const originUrl = remoteRes.stdout.trim();
371
+ // Compare host of originUrl vs canonicalUrl (we parse both)
372
+ try {
373
+ // originUrl may be canonical as stored; but if clone followed redirect, originUrl might still be original.
374
+ // To detect redirect, we could ask git for http effective URL via trace, but for now we compare if origin differs
375
+ // by host/path — if origin host != canonical host => trust violation
376
+ const orig = locator.canonicalUrl;
377
+ if (originUrl !== orig) {
378
+ // Parse both to compare hosts (simple string compare host extraction)
379
+ const parseHost = (u: string): string | null => {
380
+ try {
381
+ if (u.includes('://')) return new URL(u).hostname.toLowerCase();
382
+ const m = u.match(/@([^:]+):/);
383
+ return m ? m[1].toLowerCase() : null;
384
+ } catch { return null; }
385
+ };
386
+ const oh = parseHost(originUrl);
387
+ const ch = locator.host;
388
+ if (oh && oh !== ch) {
389
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
390
+ return {
391
+ ok: false,
392
+ findings: [
393
+ trustFinding(
394
+ scope,
395
+ CODE.GIT_TRUST_REDIRECT,
396
+ RULE.GIT_TRUST_REDIRECT,
397
+ `Acquisition Trust Base violation: canonical-locator-changing redirect — origin '${originUrl}' host '${oh}' differs from requested '${ch}'`,
398
+ ),
399
+ ],
400
+ };
401
+ }
402
+ }
403
+ } catch {}
404
+ }
405
+ }
406
+
407
+ // For commit selector, verify that the commit exists in the cloned repo (fetch if needed)
408
+ // For branch/tag/default we already resolved via ls-remote, but we still need to fetch the object.
409
+ // After clone --no-checkout, the remote refs are available, but for commit we may need to fetch directly.
410
+ if (selector.kind === 'commit') {
411
+ // Try to verify commit exists; if not, fetch it
412
+ const catRes = await executor([...configArgs, '-C', dest, 'cat-file', '-e', `${sha}^{commit}`], { env });
413
+ if (catRes.exitCode !== 0) {
414
+ // Attempt fetch of that specific sha (server may allow)
415
+ const fetchRes = await executor([...configArgs, '-C', dest, 'fetch', 'origin', sha], { env });
416
+ if (fetchRes.exitCode !== 0) {
417
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
418
+ return {
419
+ ok: false,
420
+ findings: [acquireFinding(scope, `commit ${sha} not found at ${locator.canonicalUrl}: ${fetchRes.stderr.trim() || catRes.stderr.trim()}`)],
421
+ stderr: fetchRes.stderr,
422
+ };
423
+ }
424
+ const cat2 = await executor([...configArgs, '-C', dest, 'cat-file', '-e', `${sha}^{commit}`], { env });
425
+ if (cat2.exitCode !== 0) {
426
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
427
+ return {
428
+ ok: false,
429
+ findings: [acquireFinding(scope, `commit ${sha} still not resolvable after fetch: ${cat2.stderr.trim()}`)],
430
+ stderr: cat2.stderr,
431
+ };
432
+ }
433
+ }
434
+ } else {
435
+ // For branch/tag/default, ensure the resolved sha is fetchable: try to fetch that ref specifically if not already present
436
+ // After clone, we can try to fetch the sha directly as well to ensure we have the object
437
+ const catRes = await executor([...configArgs, '-C', dest, 'cat-file', '-e', `${sha}^{commit}`], { env });
438
+ if (catRes.exitCode !== 0) {
439
+ const fetchRes = await executor([...configArgs, '-C', dest, 'fetch', 'origin', sha], { env });
440
+ if (fetchRes.exitCode !== 0) {
441
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
442
+ return {
443
+ ok: false,
444
+ findings: [acquireFinding(scope, `resolved revision ${sha} not fetchable: ${fetchRes.stderr.trim()}`)],
445
+ stderr: fetchRes.stderr,
446
+ };
447
+ }
448
+ }
449
+ }
450
+
451
+ // Materialize the tree at the resolved revision without running hooks/filters/submodules
452
+ // Use checkout with hardened config: core.hooksPath already disabled, lfs filters disabled
453
+ // We do: git -C <dest> checkout --force <sha> -- (or git checkout <sha> --)
454
+ // Since we used --no-checkout, HEAD is not yet at sha; we checkout.
455
+ // To avoid checking out with smudge, we already disabled filters via config.
456
+ const checkoutRes = await executor([...configArgs, '-C', dest, 'checkout', '--force', sha, '--'], { env });
457
+ if (checkoutRes.exitCode !== 0) {
458
+ // Fallback: try `git -C dest checkout -f sha`
459
+ const checkout2 = await executor([...configArgs, '-C', dest, 'checkout', '-f', sha], { env });
460
+ if (checkout2.exitCode !== 0) {
461
+ if (createdTemp) try { rmSync(dest, { recursive: true, force: true }); } catch {}
462
+ return {
463
+ ok: false,
464
+ findings: [acquireFinding(scope, `failed to checkout resolved revision ${sha}: ${checkoutRes.stderr.trim() || checkout2.stderr.trim()}`)],
465
+ stderr: checkoutRes.stderr,
466
+ };
467
+ }
468
+ }
469
+
470
+ // Ensure we did not accidentally recurse submodules (we never passed --recurse-submodules, so safe)
471
+ // Also ensure .git/hooks not executed — we used core.hooksPath=/dev/null
472
+
473
+ return { ok: true, acquiredPath: dest, resolvedRevision: sha, findings: [], createdTemp };
474
+ }
475
+
476
+ /** Cleanup helper for acquired path when caller is done */
477
+ export function cleanupAcquisition(path: string): void {
478
+ try {
479
+ if (path && existsSync(path)) rmSync(path, { recursive: true, force: true });
480
+ } catch {}
481
+ }