@qijenchen/governance 0.1.0-beta.94

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 (40) hide show
  1. package/README.md +89 -0
  2. package/bin/attest.mjs +4 -0
  3. package/bin/check.mjs +4 -0
  4. package/bin/doctor.mjs +4 -0
  5. package/bin/generate.mjs +4 -0
  6. package/bin/governance.mjs +4 -0
  7. package/bin/hook.mjs +4 -0
  8. package/bin/upgrade.mjs +4 -0
  9. package/canonical/gates.json +42 -0
  10. package/canonical/manifest.json +476 -0
  11. package/canonical/plugin-aliases.json +25 -0
  12. package/canonical/provider-lifecycle.json +48 -0
  13. package/canonical/providers.json +455 -0
  14. package/canonical/roles.json +12 -0
  15. package/canonical/rules.json +81 -0
  16. package/canonical/schemas/attestation.schema.json +61 -0
  17. package/canonical/schemas/diagnostic.schema.json +37 -0
  18. package/canonical/schemas/gates.schema.json +27 -0
  19. package/canonical/schemas/lock.schema.json +189 -0
  20. package/canonical/schemas/manifest.schema.json +103 -0
  21. package/canonical/schemas/plugin-aliases.schema.json +59 -0
  22. package/canonical/schemas/provider-hook-coverage.schema.json +173 -0
  23. package/canonical/schemas/provider-lifecycle.schema.json +126 -0
  24. package/canonical/schemas/providers.schema.json +917 -0
  25. package/canonical/schemas/roles.schema.json +27 -0
  26. package/canonical/schemas/rules.schema.json +46 -0
  27. package/canonical/schemas/upgrade-plan.schema.json +31 -0
  28. package/package.json +42 -0
  29. package/src/authority-decision-evidence.mjs +413 -0
  30. package/src/canonical-order.mjs +8 -0
  31. package/src/carrier-projection.mjs +407 -0
  32. package/src/cli.mjs +114 -0
  33. package/src/closed-tool-execution.mjs +1001 -0
  34. package/src/common.mjs +278 -0
  35. package/src/contract.mjs +760 -0
  36. package/src/hook-api.mjs +107 -0
  37. package/src/index.mjs +14 -0
  38. package/src/provider-hook-normalization.mjs +1646 -0
  39. package/src/provider-review-binding.mjs +2377 -0
  40. package/src/snapshot.mjs +520 -0
@@ -0,0 +1,1001 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ constants as fsConstants,
7
+ existsSync,
8
+ fstatSync,
9
+ fsyncSync,
10
+ lstatSync,
11
+ mkdtempSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ readlinkSync,
16
+ readSync,
17
+ realpathSync,
18
+ rmSync,
19
+ statSync,
20
+ symlinkSync,
21
+ writeFileSync,
22
+ } from 'node:fs'
23
+ import { tmpdir } from 'node:os'
24
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
25
+
26
+ const CLOSED_GIT_EXECUTABLES = Object.freeze({
27
+ darwin: '/usr/bin/git',
28
+ linux: '/usr/bin/git',
29
+ })
30
+ const CLOSED_GIT_GLOBAL_ARGUMENTS = Object.freeze([
31
+ '--no-optional-locks',
32
+ '-c', 'core.autocrlf=false',
33
+ '-c', 'core.fsmonitor=false',
34
+ '-c', 'core.hooksPath=/dev/null',
35
+ '-c', 'credential.helper=',
36
+ '-c', 'diff.external=',
37
+ '-c', 'core.attributesFile=/dev/null',
38
+ '-c', 'core.excludesFile=/dev/null',
39
+ '-c', 'core.untrackedCache=false',
40
+ '-c', 'init.templateDir=',
41
+ ])
42
+ const CLOSED_GIT_ENVIRONMENT = Object.freeze({
43
+ GIT_CONFIG_GLOBAL: '/dev/null',
44
+ GIT_CONFIG_NOSYSTEM: '1',
45
+ GIT_NO_LAZY_FETCH: '1',
46
+ GIT_NO_REPLACE_OBJECTS: '1',
47
+ GIT_OPTIONAL_LOCKS: '0',
48
+ GIT_PAGER: '/bin/cat',
49
+ GIT_TERMINAL_PROMPT: '0',
50
+ HOME: '/dev/null',
51
+ LANG: 'C',
52
+ LC_ALL: 'C',
53
+ PATH: '/usr/bin:/bin',
54
+ XDG_CONFIG_HOME: '/dev/null',
55
+ })
56
+ const CLOSED_GH_CANDIDATES = Object.freeze({
57
+ darwin: ['/opt/homebrew/bin/gh', '/usr/local/bin/gh', '/usr/bin/gh'],
58
+ linux: ['/usr/bin/gh', '/usr/local/bin/gh', '/home/linuxbrew/.linuxbrew/bin/gh'],
59
+ })
60
+ const CLOSED_PRIVATE_RUNTIME_BASES = Object.freeze({
61
+ darwin: '/private/tmp',
62
+ linux: '/tmp',
63
+ })
64
+ const CLOSED_HOOK_EXECUTABLE_CANDIDATES = Object.freeze({
65
+ darwin: Object.freeze({
66
+ bash: Object.freeze(['/bin/bash']),
67
+ git: Object.freeze(['/usr/bin/git']),
68
+ jq: Object.freeze(['/usr/bin/jq']),
69
+ python3: Object.freeze(['/usr/bin/python3']),
70
+ }),
71
+ linux: Object.freeze({
72
+ bash: Object.freeze(['/usr/bin/bash', '/bin/bash']),
73
+ git: Object.freeze(['/usr/bin/git']),
74
+ jq: Object.freeze(['/usr/bin/jq']),
75
+ python3: Object.freeze(['/usr/bin/python3']),
76
+ }),
77
+ })
78
+ const CLOSED_HOOK_SYSTEM_PATH_CANDIDATES = Object.freeze([
79
+ '/usr/bin',
80
+ '/bin',
81
+ '/usr/sbin',
82
+ '/sbin',
83
+ ])
84
+ const CLOSED_GH_ARGUMENT_PREFIX = Object.freeze([])
85
+ const DANGEROUS_LOCAL_GIT_CONFIG = /^(?:alias\.|credential\.|filter\.|gpg\.|http\.|include\.|includeif\.|merge\..+\.driver$|protocol\..+\.allow$|submodule\.|tar\..+\.command$|url\..+\.(?:insteadof|pushinsteadof)$|core\.(?:alternaterefscommand|askpass|attributesfile|editor|excludesfile|gitproxy|hookspath|pager|sshcommand|worktree)$|extensions\.worktreeconfig$|interactive\.difffilter$|remote\..+\.(?:proxy|receivepack|uploadpack|vcs)$|sequence\.editor$)|^diff\..+\.(?:command|textconv)$/
86
+
87
+ const cachedGhExecutables = new Map()
88
+ const cachedHookToolPaths = new Map()
89
+ let ghCleanupRegistered = false
90
+ const ghCleanupRoots = new Set()
91
+ const MAX_CLOSED_EXECUTABLE_BYTES = 512 * 1024 * 1024
92
+ const MAX_CANONICAL_GIT_HOOK_BYTES = 1024 * 1024
93
+ const MAX_CLOSED_GIT_OUTPUT_BYTES = 256 * 1024 * 1024
94
+
95
+ function invariant(condition, message) {
96
+ if (!condition) throw new Error(message)
97
+ }
98
+
99
+ function sha256(bytes) {
100
+ return createHash('sha256').update(bytes).digest('hex')
101
+ }
102
+
103
+ function isContained(root, target) {
104
+ const rel = relative(root, target)
105
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
106
+ }
107
+
108
+ function sameIdentity(left, right) {
109
+ return left.dev === right.dev
110
+ && left.ino === right.ino
111
+ && left.mode === right.mode
112
+ && left.nlink === right.nlink
113
+ && left.size === right.size
114
+ && left.mtimeNs === right.mtimeNs
115
+ && left.ctimeNs === right.ctimeNs
116
+ }
117
+
118
+ function sameDirectoryIdentity(left, right) {
119
+ return left.dev === right.dev
120
+ && left.ino === right.ino
121
+ && left.mode === right.mode
122
+ && left.uid === right.uid
123
+ && left.gid === right.gid
124
+ }
125
+
126
+ function validateClosedRuntimeBaseCandidate(candidate, runtimePlatform) {
127
+ const configured = candidate
128
+ invariant(configured, `Closed private runtime is unsupported on platform ${runtimePlatform}`)
129
+ const canonical = realpathSync(configured)
130
+ const info = lstatSync(canonical, { bigint: true })
131
+ const currentUid = typeof process.getuid === 'function' ? BigInt(process.getuid()) : null
132
+ const rootStickyOrClosed = info.uid === 0n
133
+ && (
134
+ (info.mode & 0o022n) === 0n
135
+ || (info.mode & 0o1000n) === 0o1000n
136
+ )
137
+ const ownedPrivate = currentUid !== null
138
+ && info.uid === currentUid
139
+ && (info.mode & 0o022n) === 0n
140
+ invariant(
141
+ info.isDirectory()
142
+ && !info.isSymbolicLink()
143
+ && (rootStickyOrClosed || ownedPrivate),
144
+ `Closed private runtime base is unsafe at ${canonical}`,
145
+ )
146
+ return canonical
147
+ }
148
+
149
+ function validateClosedPrivateRuntimeBase(runtimePlatform) {
150
+ // Honor the standard per-user temporary directory (TMPDIR / os.tmpdir()) before the
151
+ // platform fallback. A hardcoded /private/tmp breaks sandboxed and CI environments where
152
+ // only TMPDIR is writable; a per-user 0700 directory is at least as private as a
153
+ // root-owned sticky directory.
154
+ return validateClosedRuntimeBaseCandidate(
155
+ tmpdir() || CLOSED_PRIVATE_RUNTIME_BASES[runtimePlatform],
156
+ runtimePlatform,
157
+ )
158
+ }
159
+
160
+ function validateClosedPlatformRuntimeBase(runtimePlatform) {
161
+ return validateClosedRuntimeBaseCandidate(CLOSED_PRIVATE_RUNTIME_BASES[runtimePlatform], runtimePlatform)
162
+ }
163
+
164
+ export function resolveClosedPrivateRuntimeBase(runtimePlatform = process.platform) {
165
+ return validateClosedPrivateRuntimeBase(runtimePlatform)
166
+ }
167
+
168
+ function stableExecutableDigest(path, maxBytes = MAX_CLOSED_EXECUTABLE_BYTES) {
169
+ let descriptor
170
+ try {
171
+ invariant(Number.isSafeInteger(maxBytes) && maxBytes > 0 && maxBytes <= MAX_CLOSED_EXECUTABLE_BYTES, 'Closed executable byte limit is invalid')
172
+ const pathBefore = lstatSync(path, { bigint: true })
173
+ descriptor = openSync(
174
+ path,
175
+ fsConstants.O_RDONLY
176
+ | (fsConstants.O_CLOEXEC ?? 0)
177
+ | (fsConstants.O_NOFOLLOW ?? 0),
178
+ )
179
+ const before = fstatSync(descriptor, { bigint: true })
180
+ invariant(
181
+ sameIdentity(pathBefore, before)
182
+ && before.isFile()
183
+ && before.size > 0n
184
+ && before.size <= BigInt(maxBytes),
185
+ `Closed executable size is outside the safe range at ${path}`,
186
+ )
187
+ const digest = createHash('sha256')
188
+ const chunk = Buffer.allocUnsafe(1024 * 1024)
189
+ let total = 0n
190
+ while (true) {
191
+ const count = readSync(descriptor, chunk, 0, chunk.length, null)
192
+ if (count === 0) break
193
+ digest.update(chunk.subarray(0, count))
194
+ total += BigInt(count)
195
+ }
196
+ const after = fstatSync(descriptor, { bigint: true })
197
+ const pathAfter = lstatSync(path, { bigint: true })
198
+ invariant(
199
+ sameIdentity(before, after)
200
+ && sameIdentity(after, pathAfter)
201
+ && total === before.size,
202
+ `Closed executable changed while it was authenticated at ${path}`,
203
+ )
204
+ return Object.freeze({
205
+ info: after,
206
+ sha256: digest.digest('hex'),
207
+ })
208
+ } finally {
209
+ if (descriptor !== undefined) closeSync(descriptor)
210
+ }
211
+ }
212
+
213
+ function canonicalRepositoryRoot(repoRoot) {
214
+ if (repoRoot === undefined) return null
215
+ invariant(
216
+ typeof repoRoot === 'string' && resolve(repoRoot) === repoRoot,
217
+ 'Closed tool repository root must be an absolute normalized path',
218
+ )
219
+ const canonical = realpathSync(repoRoot)
220
+ const info = lstatSync(canonical)
221
+ invariant(
222
+ canonical === repoRoot && info.isDirectory() && !info.isSymbolicLink(),
223
+ 'Closed tool repository root must be one canonical real directory',
224
+ )
225
+ return canonical
226
+ }
227
+
228
+ function validateClosedHookExecutableSource({
229
+ candidate,
230
+ label,
231
+ repoRoot,
232
+ runtimePlatform,
233
+ allowCurrentOwner = false,
234
+ }) {
235
+ invariant(
236
+ typeof candidate === 'string' && isAbsolute(candidate) && resolve(candidate) === candidate,
237
+ `Closed ${label} executable candidate must be one absolute normalized path`,
238
+ )
239
+ const source = realpathSync(candidate)
240
+ const currentUid = typeof process.getuid === 'function' ? BigInt(process.getuid()) : null
241
+ const authenticated = stableExecutableDigest(source)
242
+ const info = authenticated.info
243
+ invariant(
244
+ info.isFile()
245
+ && !info.isSymbolicLink()
246
+ && (info.mode & 0o111n) !== 0n
247
+ && (info.mode & 0o022n) === 0n
248
+ && (
249
+ info.uid === 0n
250
+ || (
251
+ allowCurrentOwner
252
+ && currentUid !== null
253
+ && info.uid === currentUid
254
+ && info.nlink === 1n
255
+ )
256
+ ),
257
+ `Closed ${label} executable source is unsafe at ${source}`,
258
+ )
259
+ if (repoRoot) {
260
+ invariant(
261
+ !isContained(repoRoot, source),
262
+ `Closed ${label} executable source cannot be inside the governed repository`,
263
+ )
264
+ }
265
+ return Object.freeze({
266
+ label,
267
+ runtimePlatform,
268
+ sha256: authenticated.sha256,
269
+ source,
270
+ sourceInfo: info,
271
+ })
272
+ }
273
+
274
+ function validateClosedRunningNodeExecutable({ candidate, repoRoot, runtimePlatform }) {
275
+ invariant(
276
+ typeof candidate === 'string' && isAbsolute(candidate) && resolve(candidate) === candidate,
277
+ 'Closed node executable candidate must be one absolute normalized path',
278
+ )
279
+ const source = realpathSync(candidate)
280
+ // The coherent provenance invariant for the hook-child interpreter is IDENTITY with the
281
+ // interpreter already executing this governance code: realpath(candidate) ==
282
+ // realpath(process.execPath), TOCTOU-bound below by inode identity plus a full content
283
+ // digest of exactly the bytes that will run. A blessed ownership/mode class is the wrong
284
+ // invariant for the running interpreter: managed GitHub runners ship node world-writable
285
+ // by image design (actions/runner-images install-nodejs.sh runs `chmod -R 777
286
+ // /usr/local/bin`; configure-system.sh runs `chmod -R 777 /opt`, covering the
287
+ // setup-node toolcache), so no ownership/mode class can admit the actual interpreter
288
+ // there — and rejecting the very binary whose in-process code performs the check adds no
289
+ // assurance, because a compromised running interpreter could bypass any in-process
290
+ // check. Foreign node candidates (explicitly configured paths that are NOT the running
291
+ // interpreter) keep the full blessed-provenance validation.
292
+ if (source !== realpathSync(process.execPath)) {
293
+ return validateClosedHookExecutableSource({
294
+ candidate,
295
+ label: 'node',
296
+ repoRoot,
297
+ runtimePlatform,
298
+ allowCurrentOwner: true,
299
+ })
300
+ }
301
+ const authenticated = stableExecutableDigest(source)
302
+ const info = authenticated.info
303
+ invariant(
304
+ info.isFile()
305
+ && !info.isSymbolicLink()
306
+ && (info.mode & 0o111n) !== 0n,
307
+ `Closed node executable source is unsafe at ${source}`,
308
+ )
309
+ if (repoRoot) {
310
+ invariant(
311
+ !isContained(repoRoot, source),
312
+ 'Closed node executable source cannot be inside the governed repository',
313
+ )
314
+ }
315
+ return Object.freeze({
316
+ label: 'node',
317
+ runtimePlatform,
318
+ sha256: authenticated.sha256,
319
+ source,
320
+ sourceInfo: info,
321
+ })
322
+ }
323
+
324
+ function resolveClosedHookExecutable({
325
+ label,
326
+ nodeExecutable,
327
+ repoRoot,
328
+ runtimePlatform,
329
+ }) {
330
+ if (label === 'node') {
331
+ return validateClosedRunningNodeExecutable({
332
+ candidate: nodeExecutable,
333
+ repoRoot,
334
+ runtimePlatform,
335
+ })
336
+ }
337
+ const candidates = CLOSED_HOOK_EXECUTABLE_CANDIDATES[runtimePlatform]?.[label]
338
+ invariant(candidates, `Closed ${label} execution is unsupported on platform ${runtimePlatform}`)
339
+ for (const candidate of candidates) {
340
+ if (!existsSync(candidate)) continue
341
+ try {
342
+ return validateClosedHookExecutableSource({
343
+ candidate,
344
+ label,
345
+ repoRoot,
346
+ runtimePlatform,
347
+ })
348
+ } catch {}
349
+ }
350
+ throw new Error(`No authenticated ${label} executable is available from the closed platform candidates`)
351
+ }
352
+
353
+ function resolveClosedHookSystemPaths(runtimePlatform) {
354
+ invariant(
355
+ CLOSED_HOOK_EXECUTABLE_CANDIDATES[runtimePlatform],
356
+ `Closed hook tool execution is unsupported on platform ${runtimePlatform}`,
357
+ )
358
+ const records = new Map()
359
+ for (const candidate of CLOSED_HOOK_SYSTEM_PATH_CANDIDATES) {
360
+ if (!existsSync(candidate)) continue
361
+ const canonical = realpathSync(candidate)
362
+ if (records.has(canonical)) continue
363
+ const info = lstatSync(canonical, { bigint: true })
364
+ invariant(
365
+ info.isDirectory()
366
+ && !info.isSymbolicLink()
367
+ && info.uid === 0n
368
+ && (info.mode & 0o022n) === 0n,
369
+ `Closed hook system PATH entry is unsafe at ${canonical}`,
370
+ )
371
+ records.set(canonical, Object.freeze({ path: canonical, info }))
372
+ }
373
+ invariant(records.size > 0, 'Closed hook system PATH has no authenticated directory')
374
+ return Object.freeze([...records.values()])
375
+ }
376
+
377
+ function closedGitIdentityEnvironment(identity) {
378
+ if (identity === undefined) return {}
379
+ invariant(identity && typeof identity === 'object' && !Array.isArray(identity), 'Closed Git identity must be an object')
380
+ const expected = [
381
+ 'authorDate',
382
+ 'authorEmail',
383
+ 'authorName',
384
+ 'committerDate',
385
+ 'committerEmail',
386
+ 'committerName',
387
+ ]
388
+ invariant(
389
+ JSON.stringify(Object.keys(identity).sort()) === JSON.stringify(expected),
390
+ 'Closed Git identity has an open or incomplete shape',
391
+ )
392
+ for (const key of expected) {
393
+ invariant(
394
+ typeof identity[key] === 'string'
395
+ && identity[key].length > 0
396
+ && identity[key].length <= 320
397
+ && !/[\0\r\n]/.test(identity[key]),
398
+ `Closed Git identity ${key} is invalid`,
399
+ )
400
+ }
401
+ invariant(
402
+ !Number.isNaN(Date.parse(identity.authorDate))
403
+ && !Number.isNaN(Date.parse(identity.committerDate)),
404
+ 'Closed Git identity dates are invalid',
405
+ )
406
+ return {
407
+ GIT_AUTHOR_DATE: identity.authorDate,
408
+ GIT_AUTHOR_EMAIL: identity.authorEmail,
409
+ GIT_AUTHOR_NAME: identity.authorName,
410
+ GIT_COMMITTER_DATE: identity.committerDate,
411
+ GIT_COMMITTER_EMAIL: identity.committerEmail,
412
+ GIT_COMMITTER_NAME: identity.committerName,
413
+ }
414
+ }
415
+
416
+ export function validateClosedGitExecutable(executable) {
417
+ invariant(typeof executable === 'string' && resolve(executable) === executable, 'Closed Git executable must be an absolute normalized path')
418
+ let info
419
+ try {
420
+ info = lstatSync(executable)
421
+ } catch (error) {
422
+ throw new Error(`Closed Git executable is unavailable at ${executable}: ${error.message}`, { cause: error })
423
+ }
424
+ invariant(
425
+ info.isFile()
426
+ && !info.isSymbolicLink()
427
+ && info.uid === 0
428
+ && (info.mode & 0o111) !== 0
429
+ && (info.mode & 0o022) === 0
430
+ && realpathSync(executable) === executable,
431
+ `Closed Git executable is unsafe at ${executable}`,
432
+ )
433
+ return executable
434
+ }
435
+
436
+ export function resolveClosedGitExecutable(runtimePlatform = process.platform) {
437
+ const executable = CLOSED_GIT_EXECUTABLES[runtimePlatform]
438
+ invariant(executable, `Closed Git execution is unsupported on platform ${runtimePlatform}`)
439
+ return validateClosedGitExecutable(executable)
440
+ }
441
+
442
+ export function runClosedGit(args, {
443
+ cwd,
444
+ gitIdentity,
445
+ input,
446
+ maxInputBytes = 64 * 1024 * 1024,
447
+ output = 'capture',
448
+ maxOutputBytes = 8 * 1024 * 1024,
449
+ runner = spawnSync,
450
+ runtimePlatform = process.platform,
451
+ timeoutMs = 5000,
452
+ } = {}) {
453
+ invariant(Array.isArray(args) && args.length > 0 && args.every(value => typeof value === 'string'), 'Closed Git arguments must be a non-empty string array')
454
+ invariant(
455
+ !args.some(value => value === '-c'
456
+ || value.startsWith('-c')
457
+ || value === '--config-env'
458
+ || value.startsWith('--config-env=')),
459
+ 'Closed Git callers may not inject global configuration arguments',
460
+ )
461
+ invariant(typeof cwd === 'string' && resolve(cwd) === cwd, 'Closed Git cwd must be one absolute normalized path')
462
+ const cwdInfo = lstatSync(cwd)
463
+ invariant(
464
+ cwdInfo.isDirectory() && !cwdInfo.isSymbolicLink() && realpathSync(cwd) === cwd,
465
+ 'Closed Git cwd must be one canonical real directory',
466
+ )
467
+ invariant(['buffer', 'capture', 'ignore'].includes(output), 'Closed Git output mode is unsupported')
468
+ invariant(Number.isInteger(maxInputBytes) && maxInputBytes > 0 && maxInputBytes <= 128 * 1024 * 1024, 'Closed Git input limit is outside the safe range')
469
+ invariant(Number.isInteger(maxOutputBytes) && maxOutputBytes > 0 && maxOutputBytes <= MAX_CLOSED_GIT_OUTPUT_BYTES, 'Closed Git output limit is outside the safe range')
470
+ invariant(Number.isInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= 30_000, 'Closed Git timeout is outside the safe range')
471
+ invariant(typeof runner === 'function', 'Closed Git runner must be callable')
472
+ invariant(
473
+ input === undefined || typeof input === 'string' || Buffer.isBuffer(input),
474
+ 'Closed Git input must be a string or Buffer',
475
+ )
476
+ invariant(
477
+ input === undefined || Buffer.byteLength(input) <= maxInputBytes,
478
+ 'Closed Git input exceeds the closed size limit',
479
+ )
480
+ return runner(
481
+ resolveClosedGitExecutable(runtimePlatform),
482
+ [...CLOSED_GIT_GLOBAL_ARGUMENTS, ...args],
483
+ {
484
+ cwd,
485
+ encoding: output === 'capture' ? 'utf8' : (output === 'buffer' ? null : undefined),
486
+ env: {
487
+ ...CLOSED_GIT_ENVIRONMENT,
488
+ ...closedGitIdentityEnvironment(gitIdentity),
489
+ },
490
+ input,
491
+ maxBuffer: maxOutputBytes,
492
+ shell: false,
493
+ stdio: output === 'ignore'
494
+ ? [input === undefined ? 'ignore' : 'pipe', 'ignore', 'ignore']
495
+ : [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
496
+ timeout: timeoutMs,
497
+ windowsHide: true,
498
+ },
499
+ )
500
+ }
501
+
502
+ function closedGitOutput(repoRoot, args, label, options = {}) {
503
+ const result = runClosedGit(args, { cwd: resolve(repoRoot), ...options })
504
+ invariant(
505
+ !result.error && result.signal === null && result.status === 0 && typeof result.stdout === 'string',
506
+ `Cannot resolve ${label} through closed Git`,
507
+ )
508
+ return result.stdout
509
+ }
510
+
511
+ export function validateCanonicalGovernanceHook(repoRoot) {
512
+ const root = realpathSync(resolve(repoRoot))
513
+ const expectedHookRoot = join(root, '.husky')
514
+ const hookRootInfo = lstatSync(expectedHookRoot, { bigint: true })
515
+ const precommitPath = join(expectedHookRoot, 'pre-commit')
516
+ const precommitPathInfo = lstatSync(precommitPath, { bigint: true })
517
+ invariant(
518
+ precommitPathInfo.isFile() && !precommitPathInfo.isSymbolicLink(),
519
+ 'Repository-local Git hook path is not backed by the canonical executable .husky/pre-commit file',
520
+ )
521
+ const precommitSource = realpathSync(precommitPath)
522
+ const authenticated = stableExecutableDigest(precommitPath, MAX_CANONICAL_GIT_HOOK_BYTES)
523
+ const precommitInfo = authenticated.info
524
+ const currentUid = typeof process.getuid === 'function' ? BigInt(process.getuid()) : null
525
+ invariant(
526
+ hookRootInfo.isDirectory()
527
+ && !hookRootInfo.isSymbolicLink()
528
+ && (hookRootInfo.mode & 0o022n) === 0n
529
+ && currentUid !== null
530
+ && (hookRootInfo.uid === currentUid || hookRootInfo.uid === 0n)
531
+ && realpathSync(expectedHookRoot) === expectedHookRoot
532
+ && precommitSource === precommitPath
533
+ && precommitInfo.isFile()
534
+ && !precommitInfo.isSymbolicLink()
535
+ && precommitInfo.nlink === 1n
536
+ && (precommitInfo.mode & 0o111n) !== 0n
537
+ && (precommitInfo.mode & 0o022n) === 0n
538
+ && (precommitInfo.mode & 0o7000n) === 0n
539
+ && (precommitInfo.uid === currentUid || precommitInfo.uid === 0n),
540
+ 'Repository-local Git hook path is not backed by the canonical executable .husky/pre-commit file',
541
+ )
542
+ return Object.freeze({
543
+ root,
544
+ hookRoot: expectedHookRoot,
545
+ hookPath: precommitPath,
546
+ sha256: authenticated.sha256,
547
+ })
548
+ }
549
+
550
+ export function assertClosedGitLocalConfiguration(repoRoot, options = {}) {
551
+ const {
552
+ allowHookPathMigration = false,
553
+ ...gitOptions
554
+ } = options
555
+ invariant(typeof allowHookPathMigration === 'boolean', 'Closed Git hook migration option must be boolean')
556
+ const root = realpathSync(resolve(repoRoot))
557
+ const names = closedGitOutput(
558
+ root,
559
+ ['config', '--local', '--no-includes', '--null', '--name-only', '--list'],
560
+ 'repository-local Git configuration',
561
+ gitOptions,
562
+ ).split('\0').filter(Boolean)
563
+ const hookPathNames = names.filter(name => name.toLowerCase() === 'core.hookspath')
564
+ if (hookPathNames.length > 0 && !allowHookPathMigration) {
565
+ const hookPaths = closedGitOutput(
566
+ root,
567
+ ['config', '--local', '--no-includes', '--null', '--get-all', 'core.hooksPath'],
568
+ 'repository-local Git hook path',
569
+ gitOptions,
570
+ ).split('\0').filter(Boolean)
571
+ invariant(hookPathNames.length === 1 && hookPaths.length === 1, 'Repository-local Git hook path must be one exact canonical binding')
572
+ // Accept every standard husky binding shape (relative `.husky`, husky v9 `.husky/_`,
573
+ // or an absolute path) as long as it stays inside this repository. Byte-exact
574
+ // absolute-path equality broke linked worktrees, re-clones, and path renames.
575
+ const resolvedHookPath = resolve(root, hookPaths[0])
576
+ invariant(
577
+ isContained(root, resolvedHookPath),
578
+ 'Repository-local Git hook path must stay inside the repository',
579
+ )
580
+ }
581
+ const dangerous = names
582
+ .map(name => name.toLowerCase())
583
+ .filter(name => name !== 'core.hookspath')
584
+ .filter(name => DANGEROUS_LOCAL_GIT_CONFIG.test(name))
585
+ .sort()
586
+ invariant(
587
+ dangerous.length === 0,
588
+ `Repository-local Git configuration may execute unsupported external commands: ${dangerous.join(', ')}`,
589
+ )
590
+ return true
591
+ }
592
+
593
+ function ghSourceCandidates({ executable, runtimePlatform }) {
594
+ if (executable !== undefined) {
595
+ invariant(typeof executable === 'string' && isAbsolute(executable) && resolve(executable) === executable, 'Closed GitHub CLI override must be one absolute normalized path')
596
+ return [executable]
597
+ }
598
+ const candidates = CLOSED_GH_CANDIDATES[runtimePlatform]
599
+ invariant(candidates, `Closed GitHub CLI execution is unsupported on platform ${runtimePlatform}`)
600
+ return candidates
601
+ }
602
+
603
+ function validateGhSource(candidate, repoRoot) {
604
+ const source = realpathSync(candidate)
605
+ const info = lstatSync(source, { bigint: true })
606
+ const currentUid = typeof process.getuid === 'function' ? BigInt(process.getuid()) : null
607
+ invariant(
608
+ info.isFile()
609
+ && !info.isSymbolicLink()
610
+ && info.nlink === 1n
611
+ && (info.mode & 0o111n) !== 0n
612
+ && (info.mode & 0o022n) === 0n
613
+ && (info.uid === 0n || (currentUid !== null && info.uid === currentUid)),
614
+ `Closed GitHub CLI source is unsafe at ${source}`,
615
+ )
616
+ if (repoRoot) {
617
+ const canonicalRepo = realpathSync(resolve(repoRoot))
618
+ invariant(!isContained(canonicalRepo, source), 'Closed GitHub CLI source cannot be inside the governed repository')
619
+ }
620
+ return { source, info }
621
+ }
622
+
623
+ function registerPrivateCleanup(root) {
624
+ ghCleanupRoots.add(root)
625
+ if (ghCleanupRegistered) return
626
+ ghCleanupRegistered = true
627
+ process.once('exit', () => {
628
+ for (const cleanupRoot of ghCleanupRoots) {
629
+ try {
630
+ chmodSync(cleanupRoot, 0o700)
631
+ rmSync(cleanupRoot, { recursive: true, force: true })
632
+ } catch {}
633
+ }
634
+ })
635
+ }
636
+
637
+ function materializeClosedGhExecutable(options = {}) {
638
+ let selected = null
639
+ for (const candidate of ghSourceCandidates(options)) {
640
+ if (!existsSync(candidate)) continue
641
+ try {
642
+ selected = validateGhSource(candidate, options.repoRoot)
643
+ break
644
+ } catch (error) {
645
+ if (options.executable !== undefined) throw error
646
+ }
647
+ }
648
+ invariant(selected, 'No authenticated GitHub CLI executable is available from the closed platform candidates')
649
+ const cacheKey = `${options.runtimePlatform ?? process.platform}\0${selected.source}`
650
+ const cached = cachedGhExecutables.get(cacheKey)
651
+ if (cached) {
652
+ const current = lstatSync(cached.source, { bigint: true })
653
+ invariant(sameIdentity(current, cached.sourceInfo), 'Closed GitHub CLI source changed after it was authenticated')
654
+ const privateInfo = lstatSync(cached.executable, { bigint: true })
655
+ invariant(
656
+ sameIdentity(privateInfo, cached.privateInfo)
657
+ && sha256(readFileSync(cached.executable)) === cached.sha256,
658
+ 'Private GitHub CLI executable changed after it was authenticated',
659
+ )
660
+ return cached.executable
661
+ }
662
+ const temporaryBase = validateClosedPrivateRuntimeBase(options.runtimePlatform ?? process.platform)
663
+ const root = realpathSync(mkdtempSync(join(temporaryBase, 'qijenchen-closed-gh-')))
664
+ chmodSync(root, 0o700)
665
+ if (options.repoRoot) {
666
+ const canonicalRepo = realpathSync(resolve(options.repoRoot))
667
+ invariant(!isContained(canonicalRepo, root), 'Closed GitHub CLI private executable cannot be materialized inside the governed repository')
668
+ }
669
+ const executable = join(root, 'gh')
670
+ const before = lstatSync(selected.source, { bigint: true })
671
+ const bytes = readFileSync(selected.source)
672
+ const after = lstatSync(selected.source, { bigint: true })
673
+ invariant(sameIdentity(before, after), 'GitHub CLI source changed while it was authenticated')
674
+ let descriptor
675
+ try {
676
+ descriptor = openSync(executable, 'wx', 0o500)
677
+ writeFileSync(descriptor, bytes)
678
+ fsyncSync(descriptor)
679
+ closeSync(descriptor)
680
+ descriptor = undefined
681
+ } finally {
682
+ if (descriptor !== undefined) closeSync(descriptor)
683
+ }
684
+ const privateInfo = lstatSync(executable, { bigint: true })
685
+ invariant(
686
+ privateInfo.isFile()
687
+ && !privateInfo.isSymbolicLink()
688
+ && privateInfo.nlink === 1n
689
+ && (privateInfo.mode & 0o777n) === 0o500n
690
+ && privateInfo.size === BigInt(bytes.length)
691
+ && sha256(readFileSync(executable)) === sha256(bytes),
692
+ 'Private GitHub CLI executable failed authenticated materialization',
693
+ )
694
+ registerPrivateCleanup(root)
695
+ const materialized = Object.freeze({
696
+ executable,
697
+ privateInfo,
698
+ root,
699
+ sha256: sha256(bytes),
700
+ source: selected.source,
701
+ sourceInfo: after,
702
+ })
703
+ cachedGhExecutables.set(cacheKey, materialized)
704
+ return executable
705
+ }
706
+
707
+ export function materializeClosedHookToolProfile({
708
+ nodeExecutable = process.execPath,
709
+ repoRoot,
710
+ runtimePlatform = process.platform,
711
+ } = {}) {
712
+ invariant(
713
+ typeof nodeExecutable === 'string'
714
+ && isAbsolute(nodeExecutable)
715
+ && resolve(nodeExecutable) === nodeExecutable,
716
+ 'Closed hook Node executable must be one absolute normalized path',
717
+ )
718
+ const canonicalRepo = canonicalRepositoryRoot(repoRoot)
719
+ const executableRecords = Object.fromEntries(
720
+ ['bash', 'git', 'jq', 'node', 'python3'].map((label) => [
721
+ label,
722
+ resolveClosedHookExecutable({
723
+ label,
724
+ nodeExecutable,
725
+ repoRoot: canonicalRepo,
726
+ runtimePlatform,
727
+ }),
728
+ ]),
729
+ )
730
+ const systemPaths = resolveClosedHookSystemPaths(runtimePlatform)
731
+ const cacheKey = [
732
+ runtimePlatform,
733
+ canonicalRepo ?? '',
734
+ ...Object.values(executableRecords).map(record => `${record.label}:${record.source}:${record.sha256}`),
735
+ ...systemPaths.map(record => record.path),
736
+ ].join('\0')
737
+ const cached = cachedHookToolPaths.get(cacheKey)
738
+ if (cached) {
739
+ cached.verify()
740
+ return cached
741
+ }
742
+
743
+ // If the honored TMPDIR sits inside the governed repository (common in isolated test
744
+ // fixtures that pin TMPDIR to a snapshot directory), fall back to a base outside the
745
+ // repository: first the platform base, then the repository's parent directory (for
746
+ // sandboxes where only the checkout's own volume is writable).
747
+ let root = null
748
+ const baseCandidates = []
749
+ const honoredBase = validateClosedPrivateRuntimeBase(runtimePlatform)
750
+ if (!canonicalRepo || !isContained(canonicalRepo, honoredBase)) baseCandidates.push(honoredBase)
751
+ else {
752
+ baseCandidates.push(() => validateClosedPlatformRuntimeBase(runtimePlatform))
753
+ baseCandidates.push(() => dirname(canonicalRepo))
754
+ }
755
+ let lastBaseError = null
756
+ for (const candidate of baseCandidates) {
757
+ try {
758
+ const temporaryBase = typeof candidate === 'function' ? candidate() : candidate
759
+ root = realpathSync(mkdtempSync(join(temporaryBase, 'qijenchen-closed-hook-tools-')))
760
+ break
761
+ } catch (error) {
762
+ lastBaseError = error
763
+ }
764
+ }
765
+ if (!root) throw lastBaseError || new Error('Closed hook tool profile has no writable private base')
766
+ chmodSync(root, 0o700)
767
+ if (canonicalRepo) {
768
+ invariant(
769
+ !isContained(canonicalRepo, root),
770
+ 'Closed hook tool profile cannot be materialized inside the governed repository',
771
+ )
772
+ }
773
+ const homeDirectory = join(root, 'home')
774
+ const tempDirectory = join(root, 'tmp')
775
+ const directoryDescriptor = (path) => {
776
+ const descriptor = openSync(path, fsConstants.O_RDONLY | (fsConstants.O_CLOEXEC ?? 0))
777
+ try {
778
+ return fstatSync(descriptor, { bigint: true })
779
+ } finally {
780
+ closeSync(descriptor)
781
+ }
782
+ }
783
+ mkdirSync(homeDirectory, { mode: 0o700 })
784
+ mkdirSync(tempDirectory, { mode: 0o700 })
785
+ const materializedHome = realpathSync(homeDirectory)
786
+ const materializedTemp = realpathSync(tempDirectory)
787
+
788
+ const executablePaths = {}
789
+ const linkRecords = {}
790
+ for (const [label, record] of Object.entries(executableRecords)) {
791
+ const executablePath = join(root, label)
792
+ symlinkSync(record.source, executablePath)
793
+ const linkInfo = lstatSync(executablePath, { bigint: true })
794
+ invariant(
795
+ linkInfo.isSymbolicLink()
796
+ && readlinkSync(executablePath) === record.source,
797
+ `Closed ${label} executable alias failed authenticated materialization`,
798
+ )
799
+ executablePaths[label] = executablePath
800
+ linkRecords[label] = Object.freeze({
801
+ info: linkInfo,
802
+ path: executablePath,
803
+ target: record.source,
804
+ })
805
+ }
806
+ chmodSync(root, 0o500)
807
+ const rootInfo = lstatSync(root, { bigint: true })
808
+ const homeInfo = directoryDescriptor(materializedHome)
809
+ const tempInfo = directoryDescriptor(materializedTemp)
810
+ const executablePath = [root, ...systemPaths.map(record => record.path)].join(':')
811
+ const profileSha256 = sha256(JSON.stringify({
812
+ executablePath,
813
+ executables: Object.fromEntries(
814
+ Object.entries(executableRecords).map(([label, record]) => [
815
+ label,
816
+ { source: record.source, sha256: record.sha256 },
817
+ ]),
818
+ ),
819
+ runtimePlatform,
820
+ schemaVersion: 1,
821
+ systemPaths: systemPaths.map(record => record.path),
822
+ }))
823
+
824
+ const verify = () => {
825
+ const currentRoot = lstatSync(root, { bigint: true })
826
+ invariant(
827
+ sameIdentity(currentRoot, rootInfo)
828
+ && currentRoot.isDirectory()
829
+ && !currentRoot.isSymbolicLink()
830
+ && (currentRoot.mode & 0o777n) === 0o500n,
831
+ 'Closed hook tool profile root changed after it was authenticated',
832
+ )
833
+ invariant(
834
+ sameDirectoryIdentity(directoryDescriptor(materializedHome), homeInfo)
835
+ && sameDirectoryIdentity(directoryDescriptor(materializedTemp), tempInfo),
836
+ 'Closed hook private HOME or TMPDIR changed after it was authenticated',
837
+ )
838
+ for (const [label, record] of Object.entries(executableRecords)) {
839
+ const sourceInfo = lstatSync(record.source, { bigint: true })
840
+ invariant(
841
+ sameIdentity(sourceInfo, record.sourceInfo),
842
+ `Closed ${label} executable source changed after it was authenticated`,
843
+ )
844
+ const link = linkRecords[label]
845
+ const linkInfo = lstatSync(link.path, { bigint: true })
846
+ invariant(
847
+ sameIdentity(linkInfo, link.info)
848
+ && linkInfo.isSymbolicLink()
849
+ && readlinkSync(link.path) === link.target,
850
+ `Closed ${label} executable alias changed after it was authenticated`,
851
+ )
852
+ }
853
+ for (const record of systemPaths) {
854
+ invariant(
855
+ sameIdentity(lstatSync(record.path, { bigint: true }), record.info),
856
+ `Closed hook system PATH entry changed after it was authenticated:${record.path}`,
857
+ )
858
+ }
859
+ return true
860
+ }
861
+
862
+ registerPrivateCleanup(root)
863
+ const profile = Object.freeze({
864
+ executablePath,
865
+ executables: Object.freeze(executablePaths),
866
+ homeDirectory: materializedHome,
867
+ profileSha256,
868
+ root,
869
+ runtimePlatform,
870
+ sources: Object.freeze(Object.fromEntries(
871
+ Object.entries(executableRecords).map(([label, record]) => [
872
+ label,
873
+ Object.freeze({ path: record.source, sha256: record.sha256 }),
874
+ ]),
875
+ )),
876
+ tempDirectory: materializedTemp,
877
+ verify,
878
+ })
879
+ cachedHookToolPaths.set(cacheKey, profile)
880
+ profile.verify()
881
+ return profile
882
+ }
883
+
884
+ export function captureClosedGitHubToken({
885
+ token,
886
+ environment = process.env,
887
+ requireToken = true,
888
+ tokenEnvironmentName = 'GH_TOKEN',
889
+ } = {}) {
890
+ invariant(
891
+ tokenEnvironmentName === null || tokenEnvironmentName === 'GH_TOKEN' || tokenEnvironmentName === 'GITHUB_TOKEN',
892
+ 'Closed GitHub CLI token authority name is unsupported',
893
+ )
894
+ if (token !== undefined) {
895
+ invariant(typeof token === 'string', 'Closed GitHub CLI token override must be a string')
896
+ }
897
+ const ambient = ['GH_TOKEN', 'GITHUB_TOKEN']
898
+ .filter(name => typeof environment?.[name] === 'string' && environment[name] !== '')
899
+ invariant(ambient.length <= 1, 'Closed GitHub CLI received multiple token authorities')
900
+ if (token !== undefined && token !== '') {
901
+ invariant(ambient.length === 0, 'Closed GitHub CLI explicit token conflicts with an ambient token authority')
902
+ } else if (ambient.length) {
903
+ invariant(
904
+ tokenEnvironmentName !== null && ambient[0] === tokenEnvironmentName,
905
+ `Closed GitHub CLI token authority must be ${tokenEnvironmentName ?? 'an explicit token'}`,
906
+ )
907
+ }
908
+ const candidate = token !== undefined && token !== ''
909
+ ? token
910
+ : (ambient.length ? environment[ambient[0]] : null)
911
+ if (candidate === null) {
912
+ invariant(!requireToken, 'Closed GitHub CLI requires one explicit GH_TOKEN or GITHUB_TOKEN')
913
+ return null
914
+ }
915
+ invariant(
916
+ candidate.length >= 8
917
+ && candidate.length <= 4096
918
+ && !/[\0\r\n]/.test(candidate),
919
+ 'Closed GitHub CLI token is malformed',
920
+ )
921
+ return candidate
922
+ }
923
+
924
+ export function runClosedGh(args, {
925
+ cwd,
926
+ environment = process.env,
927
+ executable,
928
+ input,
929
+ maxInputBytes = 16 * 1024 * 1024,
930
+ maxOutputBytes = 16 * 1024 * 1024,
931
+ output = 'capture',
932
+ repoRoot,
933
+ requireToken = true,
934
+ runtimePlatform = process.platform,
935
+ timeoutMs = 60_000,
936
+ token,
937
+ tokenEnvironmentName = 'GH_TOKEN',
938
+ } = {}) {
939
+ invariant(Array.isArray(args) && args.length > 0 && args.every(value => typeof value === 'string'), 'Closed GitHub CLI arguments must be a non-empty string array')
940
+ invariant(typeof cwd === 'string' && resolve(cwd) === cwd, 'Closed GitHub CLI cwd must be one absolute normalized path')
941
+ invariant(['buffer', 'capture', 'ignore'].includes(output), 'Closed GitHub CLI output mode is unsupported')
942
+ invariant(Number.isInteger(maxInputBytes) && maxInputBytes > 0 && maxInputBytes <= 64 * 1024 * 1024, 'Closed GitHub CLI input limit is outside the safe range')
943
+ invariant(Number.isInteger(maxOutputBytes) && maxOutputBytes > 0 && maxOutputBytes <= 128 * 1024 * 1024, 'Closed GitHub CLI output limit is outside the safe range')
944
+ invariant(Number.isInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= 120_000, 'Closed GitHub CLI timeout is outside the safe range')
945
+ invariant(
946
+ input === undefined || typeof input === 'string' || Buffer.isBuffer(input),
947
+ 'Closed GitHub CLI input must be a string or Buffer',
948
+ )
949
+ invariant(
950
+ input === undefined || Buffer.byteLength(input) <= maxInputBytes,
951
+ 'Closed GitHub CLI input exceeds the closed size limit',
952
+ )
953
+ const githubToken = captureClosedGitHubToken({
954
+ token,
955
+ environment,
956
+ requireToken,
957
+ tokenEnvironmentName,
958
+ })
959
+ const privateExecutable = materializeClosedGhExecutable({ executable, repoRoot, runtimePlatform })
960
+ const privateHome = dirname(privateExecutable)
961
+ return spawnSync(
962
+ privateExecutable,
963
+ [...CLOSED_GH_ARGUMENT_PREFIX, ...args],
964
+ {
965
+ cwd,
966
+ encoding: output === 'capture' ? 'utf8' : (output === 'buffer' ? null : undefined),
967
+ env: {
968
+ GH_HOST: 'github.com',
969
+ GH_CONFIG_DIR: privateHome,
970
+ GH_PAGER: 'cat',
971
+ GH_PROMPT_DISABLED: '1',
972
+ ...(githubToken ? { GH_TOKEN: githubToken } : {}),
973
+ HOME: privateHome,
974
+ LANG: 'C',
975
+ LC_ALL: 'C',
976
+ NO_COLOR: '1',
977
+ PAGER: 'cat',
978
+ PATH: '/usr/bin:/bin',
979
+ XDG_CONFIG_HOME: privateHome,
980
+ },
981
+ input,
982
+ maxBuffer: maxOutputBytes,
983
+ shell: false,
984
+ stdio: output === 'ignore' ? 'ignore' : ['pipe', 'pipe', 'pipe'],
985
+ timeout: timeoutMs,
986
+ windowsHide: true,
987
+ },
988
+ )
989
+ }
990
+
991
+ export function closedToolExecutionPaths() {
992
+ return Object.freeze({
993
+ git: resolveClosedGitExecutable(),
994
+ gh: [...cachedGhExecutables.values()].map(record => record.executable).sort(),
995
+ hookProfiles: [...cachedHookToolPaths.values()].map(record => Object.freeze({
996
+ executablePath: record.executablePath,
997
+ profileSha256: record.profileSha256,
998
+ root: record.root,
999
+ })),
1000
+ })
1001
+ }