feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { join } from 'node:path'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { sandboxConfiguration } from './configuration'
|
||||
import { stageSandboxToolchains } from './toolchains'
|
||||
import { verifiedBinary } from './native-component'
|
||||
import { sealWorkspace, saveWorkspaceRecord, workspaceRecord } from './workspace'
|
||||
import { assertActive } from './policy'
|
||||
import { assertPolicyCurrent } from './policy-store'
|
||||
import type { SandboxEffectivePolicy } from '@shared/sandbox'
|
||||
|
||||
export interface GitControl {
|
||||
root: string
|
||||
repository: string
|
||||
run(args: string[], options?: { input?: Buffer; allowFailure?: boolean; network?: boolean; config?: Record<string, string>; environment?: Record<string, string>; timeoutMs?: number }): Promise<{ code: number; stdout: Buffer; stderr: string }>
|
||||
close(): Promise<void>
|
||||
}
|
||||
const activeControls = new Set<() => void>()
|
||||
export function disposeGitControls(): void { for (const cancel of [...activeControls]) cancel() }
|
||||
|
||||
/** Fixed, verified Git in a parent-monitored native Job; never a model shell. */
|
||||
export async function createGitControl(policy: SandboxEffectivePolicy, signal?: AbortSignal): Promise<GitControl> {
|
||||
assertActive(signal)
|
||||
const parent = join(sandboxConfiguration().stateRoot, 'git-control')
|
||||
await fs.mkdir(parent, { recursive: true })
|
||||
const root = await fs.mkdtemp(join(parent, 'operation-'))
|
||||
await sealWorkspace(root)
|
||||
const owner = { version: 1, root, pid: process.pid, nonce: randomUUID(), createdAt: Date.now(), state: 'active' }
|
||||
await saveWorkspaceRecord(join(root, 'owner.json'), owner, false)
|
||||
for (const name of ['workspace', 'repository', 'home', 'tmp']) await fs.mkdir(join(root, name))
|
||||
try { await stageSandboxToolchains(root, ['git'], signal) }
|
||||
catch (error) { await fs.rm(root, { recursive: true, force: true }).catch(() => undefined); throw error }
|
||||
const binary = await verifiedBinary(true, true, true), tool = join(root, 'tools/git')
|
||||
let running = false
|
||||
return {
|
||||
root, repository: join(root, 'repository'),
|
||||
async run(args, options = {}) {
|
||||
if (running) throw new Error('Git control operations must be sequential')
|
||||
if (args.length > 128 || args.some(arg => typeof arg !== 'string' || arg.includes('\0'))) throw new Error('Invalid Git control argv')
|
||||
assertActive(signal); await assertPolicyCurrent(policy)
|
||||
const config: Record<string, string> = {
|
||||
'core.hooksPath': join(tool, 'empty-hooks'), 'core.fsmonitor': 'false', 'credential.helper': '',
|
||||
'commit.gpgSign': 'false', 'tag.gpgSign': 'false', 'maintenance.auto': 'false', 'gc.auto': '0',
|
||||
'diff.autoRefreshIndex': 'false', 'core.attributesFile': 'NUL', 'core.excludesFile': 'NUL',
|
||||
'protocol.allow': 'never', 'http.followRedirects': 'false', 'http.sslVerify': 'true',
|
||||
...options.config
|
||||
}
|
||||
const win = process.env.SystemRoot ?? 'C:\\Windows'
|
||||
const environment: Record<string, string> = {
|
||||
SYSTEMROOT: win, WINDIR: win, SYSTEMDRIVE: win.slice(0, 2), COMSPEC: join(win, 'System32/cmd.exe'),
|
||||
PATH: `${tool};${join(win, 'System32')}`, HOME: join(root, 'home'), USERPROFILE: join(root, 'home'),
|
||||
TEMP: join(root, 'tmp'), TMP: join(root, 'tmp'), LC_ALL: 'C.UTF-8',
|
||||
GIT_DIR: join(root, 'repository'), GIT_WORK_TREE: join(root, 'workspace'),
|
||||
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: 'NUL', GIT_CONFIG_SYSTEM: 'NUL',
|
||||
GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0', GIT_PAGER: '', GIT_EDITOR: '', GIT_SEQUENCE_EDITOR: '',
|
||||
GIT_EXEC_PATH: tool, GIT_TEMPLATE_DIR: join(tool, 'empty-template'), GIT_NO_LAZY_FETCH: '1',
|
||||
GIT_ALLOW_PROTOCOL: options.network ? 'https:http:ssh' : '',
|
||||
GIT_CONFIG_COUNT: String(Object.keys(config).length), ...options.environment
|
||||
}
|
||||
Object.entries(config).forEach(([key, value], index) => { environment[`GIT_CONFIG_KEY_${index}`] = key; environment[`GIT_CONFIG_VALUE_${index}`] = value })
|
||||
const fields = (values: string[]): string => values.map(value => Buffer.from(value).toString('hex')).join('\n') + '\n'
|
||||
await fs.writeFile(join(root, 'arguments'), fields(args))
|
||||
await fs.writeFile(join(root, 'environment'), fields(Object.entries(environment).map(([name, value]) => `${name}=${value}`)))
|
||||
await fs.writeFile(join(root, 'input'), options.input ?? Buffer.alloc(0))
|
||||
await fs.unlink(join(root, 'native-lease.json')).catch(error => { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error })
|
||||
const timeout = options.timeoutMs ?? 120000
|
||||
running = true
|
||||
const child = spawn(binary, ['--git-control', root, String(timeout)], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const chunks: Buffer[] = []; let length = 0, stderr = '', failure: Error | undefined, checking = false
|
||||
const fail = (error: Error): void => { failure ??= error; child.kill() }
|
||||
const revoke = (): void => fail(Object.assign(new Error('Git control cancelled or policy revoked'), { name: 'AbortError' }))
|
||||
activeControls.add(revoke)
|
||||
child.stdout.on('data', (bytes: Buffer) => { length += bytes.length; if (length > 32 * 1024 * 1024) fail(new Error('Git control output exceeds limit')); else chunks.push(bytes) })
|
||||
child.stderr.on('data', (bytes: Buffer) => { stderr = (stderr + bytes.toString('utf8')).slice(-2 * 1024 * 1024) })
|
||||
const abort = (): void => fail(Object.assign(new Error('Git operation cancelled'), { name: 'AbortError' }))
|
||||
signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort()
|
||||
const timer = setTimeout(() => fail(new Error('Git control timeout')), timeout + 5000)
|
||||
const watch = setInterval(() => {
|
||||
if (checking) return
|
||||
checking = true
|
||||
void assertPolicyCurrent(policy).catch(error => fail(error as Error)).finally(() => { checking = false })
|
||||
}, 250)
|
||||
try {
|
||||
const code = await new Promise<number>((done, reject) => { child.once('error', reject); child.once('close', code => done(code ?? 130)) })
|
||||
if (failure) throw failure
|
||||
if (code !== 0 && !options.allowFailure) throw new Error(`Git control failed (${code}): ${stderr.slice(-4096)}`)
|
||||
return { code, stdout: Buffer.concat(chunks), stderr }
|
||||
} finally { clearTimeout(timer); clearInterval(watch); signal?.removeEventListener('abort', abort); activeControls.delete(revoke); running = false }
|
||||
},
|
||||
async close() {
|
||||
if (running) throw new Error('Cannot clean an active Git control Job')
|
||||
await saveWorkspaceRecord(join(root, 'owner.json'), { ...owner, state: 'closed' }).catch(() => undefined)
|
||||
await fs.rm(root, { recursive: true, force: true }).catch(async () => {
|
||||
await saveWorkspaceRecord(join(root, 'owner.json'), { ...owner, state: 'cleanup-required' }).catch(() => undefined)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupGitControls(): Promise<Array<{ id: string; state: 'removed' | 'active' | 'invalid'; reason?: string }>> {
|
||||
const parent = resolve(sandboxConfiguration().stateRoot, 'git-control'), entries = await fs.readdir(parent, { withFileTypes: true }).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error })
|
||||
if (entries.length > 10000) throw new Error('Too many Git control records')
|
||||
const alive = (pid: number): boolean => { try { process.kill(pid, 0); return true } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false; throw error } }
|
||||
const result: Array<{ id: string; state: 'removed' | 'active' | 'invalid'; reason?: string }> = []
|
||||
for (const entry of entries) {
|
||||
if (!/^operation-[a-zA-Z0-9_-]{6,}$/.test(entry.name) || !entry.isDirectory() || entry.isSymbolicLink()) { result.push({ id: entry.name, state: 'invalid' }); continue }
|
||||
const root = resolve(parent, entry.name)
|
||||
try {
|
||||
if ((await fs.realpath(dirname(root))).toLowerCase() !== (await fs.realpath(parent)).toLowerCase() || (await fs.realpath(root)).toLowerCase() !== root.toLowerCase()) throw new Error('Git cleanup root changed or escaped')
|
||||
const owner = await workspaceRecord<{ version: number; root: string; pid: number; nonce: string; state: string }>(join(root, 'owner.json'), 4096)
|
||||
if (owner.version !== 1 || owner.root.toLowerCase() !== root.toLowerCase() || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || !/^[a-f0-9-]{36}$/.test(owner.nonce)) throw new Error('Invalid Git control owner')
|
||||
if (owner.state === 'active' && alive(owner.pid)) { result.push({ id: entry.name, state: 'active' }); continue }
|
||||
const lease = await workspaceRecord<{ version: number; root: string; pid: number }>(join(root, 'native-lease.json'), 4096).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error })
|
||||
if (lease && (lease.version !== 1 || lease.root.toLowerCase() !== root.toLowerCase() || !Number.isSafeInteger(lease.pid) || lease.pid < 1)) throw new Error('Invalid native Git control lease')
|
||||
if (lease && alive(lease.pid)) { result.push({ id: entry.name, state: 'active' }); continue }
|
||||
await fs.rm(root, { recursive: true, force: false }); result.push({ id: entry.name, state: 'removed' })
|
||||
} catch (error) { result.push({ id: entry.name, state: 'invalid', reason: (error as Error).message }) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user