feat: complete WorkBuddy parity and Windows release workflow

This commit is contained in:
xiaohan.yuan committed 2026-10-18 12:45:21 +08:00
1 parent 143516b3a8
commit d5512df549
434 files changed
+54977 -1854

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
import { promises as fs } from 'node:fs'
import { createHash, randomUUID } from 'node:crypto'
import { deflate } from 'node:zlib'
import { promisify } from 'node:util'
import { dirname, isAbsolute, join, relative } from 'node:path'
import type { SandboxEffectivePolicy, SandboxGitState } from '@shared/sandbox'
import { sandboxConfiguration } from './configuration'
import { assertActive, protectedPath, within } from './policy'
import { assertPolicyCurrent } from './policy-store'
import { sealWorkspace } from './workspace'
interface GitFile { path: string; bytes: number; sha256: string }
export interface GitIdentity { id: string; manifestSha256: string }
export interface GitManifest {
version: 1; id: string; workspaceRoot: string; policyFingerprint: string
createdAt: number; contentHash: string; files: GitFile[]; bytes: number
origin?: 'host' | 'remote' | 'empty'
}
export interface SandboxGitSession {
state: SandboxGitState
capture(signal?: AbortSignal): Promise<GitIdentity | null>
assertCurrent(): Promise<void>
publish(candidate: GitIdentity): Promise<void>
close(): Promise<void>
}
const limits = { files: 30_000, bytes: 256 * 1024 * 1024, fileBytes: 128 * 1024 * 1024, manifestBytes: 8 * 1024 * 1024 }
const hash = (bytes: Buffer | string): string => createHash('sha256').update(bytes).digest('hex')
const sha1 = (bytes: Buffer): Buffer => createHash('sha1').update(bytes).digest()
const compress = promisify(deflate)
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
const hashPattern = /^[a-f0-9]{64}$/
const key = (value: string): string => process.platform === 'win32' ? value.toLowerCase() : value
const repositoryRoot = (workspace: string, namespace?: string): string => {
if (namespace !== undefined && !hashPattern.test(namespace)) throw new Error('Invalid sandbox Git draft namespace')
return join(sandboxConfiguration().stateRoot, 'git', hash(key(workspace)) + (namespace ? `-${namespace}` : ''))
}
async function directory(path: string): Promise<void> {
const stat = await fs.lstat(path)
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Sandbox Git state directory must not be a link')
}
async function regular(path: string, maximum = limits.fileBytes): Promise<Buffer> {
const stat = await fs.lstat(path)
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maximum) throw new Error('Sandbox Git metadata contains a link, special file or oversized file')
const handle = await fs.open(path, 'r')
try {
const before = await handle.stat()
if (before.ino !== stat.ino || before.dev !== stat.dev || before.nlink !== 1 || before.size !== stat.size) throw new Error('Sandbox Git file was replaced before reading')
const bytes = await handle.readFile(), after = await handle.stat()
if (bytes.length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) throw new Error('Sandbox Git file changed during reading')
return bytes
} finally { await handle.close() }
}
function filePath(value: unknown): string {
if (typeof value !== 'string' || !value || value.length > 2048 || isAbsolute(value) || value.includes('\\') || value.split('/').some(part => !part || part === '.' || part === '..') || protectedPath(value)
|| /(?:^|\/)(?:commondir|gitdir|alternates|http-alternates)$/.test(value) || /\.lock$/i.test(value)) throw new Error('Unsupported or unsafe sandbox Git metadata path')
return value
}
async function prepareRoot(workspace: string, namespace?: string): Promise<string> {
const root = repositoryRoot(workspace, namespace)
await fs.mkdir(join(root, 'objects'), { recursive: true, mode: 0o700 })
for (const path of [sandboxConfiguration().stateRoot, join(sandboxConfiguration().stateRoot, 'git'), root, join(root, 'objects')]) await directory(path)
return root
}
async function activeIdentity(root: string): Promise<GitIdentity | null> {
const path = join(root, 'active.json')
if (!await fs.lstat(path).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error })) return null
const value = JSON.parse((await regular(path, 4096)).toString('utf8')) as GitIdentity
if (!uuid.test(value.id) || !hashPattern.test(value.manifestSha256)) throw new Error('Invalid sandbox Git revision identity')
return value
}
async function manifestAt(root: string, identity: GitIdentity, workspace: string): Promise<GitManifest> {
const object = join(root, 'objects', identity.id)
await directory(object); await directory(join(object, 'files'))
const bytes = await regular(join(object, 'manifest.json'), limits.manifestBytes)
if (hash(bytes) !== identity.manifestSha256) throw new Error('Sandbox Git manifest digest mismatch')
const value = JSON.parse(bytes.toString('utf8')) as GitManifest
if (value.version !== 1 || value.id !== identity.id || typeof value.workspaceRoot !== 'string' || key(value.workspaceRoot) !== key(workspace) || !hashPattern.test(value.policyFingerprint)
|| value.origin !== undefined && !['host', 'remote', 'empty'].includes(value.origin)
|| !Array.isArray(value.files) || !value.files.length || value.files.length > limits.files) throw new Error('Invalid sandbox Git manifest ownership or format')
let total = 0
const seen = new Set<string>()
for (const file of value.files) {
const path = filePath(file.path)
if (seen.has(key(path)) || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > limits.fileBytes || !hashPattern.test(file.sha256)) throw new Error('Invalid sandbox Git file identity')
seen.add(key(path)); total += file.bytes
}
if (!seen.has(key('HEAD')) || !seen.has('config') || value.bytes !== total || total > limits.bytes || value.contentHash !== hash(JSON.stringify(value.files))) throw new Error('Invalid sandbox Git metadata inventory')
return value
}
async function scanRepository(source: string, signal?: AbortSignal): Promise<{ files: GitFile[]; bytes: number }> {
const files: GitFile[] = []
let bytes = 0, entries = 0
async function walk(path: string, depth = 0): Promise<void> {
if (depth > 64) throw new Error('Sandbox Git metadata nesting exceeds the limit')
await directory(path)
for (const entry of await fs.readdir(path, { withFileTypes: true })) {
assertActive(signal)
if (++entries > limits.files) throw new Error('Sandbox Git metadata exceeds the entry limit')
const absolute = join(path, entry.name), file = filePath(relative(source, absolute).replaceAll('\\', '/'))
if (entry.isSymbolicLink()) throw new Error('Sandbox Git metadata cannot contain links')
if (entry.isDirectory()) await walk(absolute, depth + 1)
else {
const content = await regular(absolute)
bytes += content.length
if (bytes > limits.bytes) throw new Error('Sandbox Git history exceeds the 256 MiB limit')
files.push({ path: file, bytes: content.length, sha256: hash(content) })
}
}
}
await walk(source)
files.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)))
return { files, bytes }
}
async function storeRepository(root: string, source: string, policy: SandboxEffectivePolicy, inventory: { files: GitFile[]; bytes: number }, signal?: AbortSignal, origin?: 'host' | 'remote' | 'empty'): Promise<GitIdentity> {
const id = randomUUID(), object = join(root, 'objects', id), target = join(object, 'files')
await fs.mkdir(target, { recursive: true, mode: 0o700 })
const manifest: GitManifest = { version: 1, id, workspaceRoot: policy.workspaceRoot, policyFingerprint: policy.fingerprint, createdAt: Date.now(), contentHash: hash(JSON.stringify(inventory.files)), ...(origin ? { origin } : {}), ...inventory }
for (const file of inventory.files) {
assertActive(signal)
const content = await regular(join(source, file.path))
if (content.length !== file.bytes || hash(content) !== file.sha256) throw new Error('Sandbox Git metadata changed after capture')
const destination = join(target, file.path)
await fs.mkdir(dirname(destination), { recursive: true })
await fs.writeFile(destination, content, { flag: 'wx', mode: 0o600 })
}
const bytes = Buffer.from(JSON.stringify(manifest) + '\n')
if (bytes.length > limits.manifestBytes) throw new Error('Sandbox Git manifest exceeds the limit')
const handle = await fs.open(join(object, 'manifest.json'), 'wx', 0o600)
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
return { id, manifestSha256: hash(bytes) }
}
async function publishIdentity(root: string, identity: GitIdentity): Promise<void> {
const temporary = join(root, `active-${randomUUID()}.tmp`)
try {
const handle = await fs.open(temporary, 'wx', 0o600)
try { await handle.writeFile(JSON.stringify(identity) + '\n'); await handle.sync() } finally { await handle.close() }
await fs.rename(temporary, join(root, 'active.json'))
} finally { await fs.unlink(temporary).catch(() => undefined) }
}
/** Build one synthetic commit from approved input bytes; never open the host .git. */
async function initializeRepository(stage: string, files: Map<string, { hash: string; size: number }>, signal?: AbortSignal): Promise<void> {
const target = join(stage, 'repository'), objects = new Set<string>()
await fs.mkdir(join(target, 'objects'), { recursive: true })
const put = async (name: string, bytes: Buffer | string): Promise<void> => {
const path = join(target, name)
await fs.mkdir(dirname(path), { recursive: true })
await fs.writeFile(path, bytes, { flag: 'wx', mode: 0o600 })
}
const object = async (type: string, bytes: Buffer): Promise<Buffer> => {
const content = Buffer.concat([Buffer.from(`${type} ${bytes.length}\0`), bytes]), id = sha1(content), hex = id.toString('hex')
if (!objects.has(hex)) { objects.add(hex); await put(`objects/${hex.slice(0, 2)}/${hex.slice(2)}`, await compress(content)) }
return id
}
interface Tree { files: Map<string, Buffer>; directories: Map<string, Tree> }
const tree = (): Tree => ({ files: new Map(), directories: new Map() }), root = tree()
const entries: Buffer[] = []
for (const [path, expected] of [...files].sort((a, b) => Buffer.compare(Buffer.from(a[0].replaceAll('\\', '/')), Buffer.from(b[0].replaceAll('\\', '/'))))) {
assertActive(signal)
const bytes = await regular(join(stage, 'workspace', path))
if (hash(bytes) !== expected.hash || bytes.length !== expected.size) throw new Error('Git baseline input changed')
const id = await object('blob', bytes), name = path.replaceAll('\\', '/')
const parts = name.split('/'); let parent = root
for (const part of parts.slice(0, -1)) { if (!parent.directories.has(part)) parent.directories.set(part, tree()); parent = parent.directories.get(part)! }
parent.files.set(parts.at(-1)!, id)
const nameBytes = Buffer.from(name), entrySize = 62 + nameBytes.length + 1
const entry = Buffer.alloc(Math.ceil(entrySize / 8) * 8)
entry.writeUInt32BE(0o100644, 24); entry.writeUInt32BE(bytes.length, 36); id.copy(entry, 40)
entry.writeUInt16BE(Math.min(nameBytes.length, 0xfff), 60); nameBytes.copy(entry, 62)
entries.push(entry)
}
const writeTree = async (value: Tree): Promise<Buffer> => {
const entries: Array<{ name: string; mode: string; id: Buffer }> = [...value.files].map(([name, id]) => ({ name, mode: '100644', id }))
for (const [name, child] of value.directories) entries.push({ name, mode: '40000', id: await writeTree(child) })
entries.sort((a, b) => Buffer.compare(Buffer.from(a.name + (a.mode === '40000' ? '/' : '')), Buffer.from(b.name + (b.mode === '40000' ? '/' : ''))))
return object('tree', Buffer.concat(entries.map(entry => Buffer.concat([Buffer.from(`${entry.mode} ${entry.name}\0`), entry.id]))))
}
const rootTree = await writeTree(root), timestamp = Math.floor(Date.now() / 1000)
const commit = await object('commit', Buffer.from(`tree ${rootTree.toString('hex')}\nauthor YCode Sandbox <sandbox@ycode.local> ${timestamp} +0000\ncommitter YCode Sandbox <sandbox@ycode.local> ${timestamp} +0000\n\nYCode sandbox baseline (filtered workspace snapshot; no host history)\n`))
const header = Buffer.alloc(12); header.write('DIRC'); header.writeUInt32BE(2, 4); header.writeUInt32BE(entries.length, 8)
const index = Buffer.concat([header, ...entries])
await put('index', Buffer.concat([index, sha1(index)]))
await put('HEAD', 'ref: refs/heads/ycode-sandbox\n')
await put('refs/heads/ycode-sandbox', `${commit.toString('hex')}\n`)
// The baseline records snapshot bytes, rather than host clean/smudge rules.
await put('info/attributes', '* -text -filter -ident -eol -working-tree-encoding\n')
await put('config', '[core]\n\trepositoryformatversion = 0\n\tbare = false\n\tfilemode = false\n\tautocrlf = false\n\tignorecase = true\n\tsymlinks = false\n[user]\n\tname = YCode Sandbox\n\temail = sandbox@ycode.local\n')
}
export async function stageSandboxGit(stage: string, policy: SandboxEffectivePolicy, files: Map<string, { hash: string; size: number }>, signal?: AbortSignal, namespace?: string): Promise<SandboxGitSession> {
const root = await prepareRoot(policy.workspaceRoot, namespace), lockPath = join(root, 'repository.lock')
const lock = await fs.open(lockPath, 'wx', 0o600).catch(error => { if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error('Sandbox Git is in use; inspect repository.lock if the previous host exited'); throw error })
const close = async (): Promise<void> => { await lock.close(); await fs.unlink(lockPath) }
try {
await lock.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }) + '\n')
let identity = await activeIdentity(root), initialized = false
const target = join(stage, 'repository')
if (!identity) {
await initializeRepository(stage, files, signal)
identity = await storeRepository(root, target, policy, await scanRepository(target, signal), signal)
await assertPolicyCurrent(policy)
await publishIdentity(root, identity)
initialized = true
} else {
const manifest = await manifestAt(root, identity, policy.workspaceRoot)
if (manifest.policyFingerprint !== policy.fingerprint) throw new Error('Sandbox Git policy changed; inspect git-state and explicitly git-reset before reusing history')
await fs.mkdir(join(target, 'objects'), { recursive: true }); await fs.mkdir(join(target, 'refs'), { recursive: true })
const source = join(root, 'objects', identity.id, 'files'), checked = new Set<string>([source])
for (const file of manifest.files) {
assertActive(signal)
const path = join(source, file.path); let parent = dirname(path)
while (!checked.has(parent)) { if (!within(source, parent)) throw new Error('Sandbox Git path escaped state'); await directory(parent); checked.add(parent); parent = dirname(parent) }
const bytes = await regular(path)
if (bytes.length !== file.bytes || hash(bytes) !== file.sha256) throw new Error('Sandbox Git metadata digest mismatch')
await fs.mkdir(dirname(join(target, file.path)), { recursive: true })
await fs.writeFile(join(target, file.path), bytes, { flag: 'wx', mode: 0o600 })
}
}
const before = identity, manifest = await manifestAt(root, before, policy.workspaceRoot)
const mode = manifest.origin === 'host' ? 'host-mirror' : manifest.origin === 'remote' ? 'remote-clone' : manifest.origin === 'empty' ? 'new-repository' : 'private-snapshot'
const state: SandboxGitState = { mode, baseline: ({ 'host-mirror': 'host-repository', 'remote-clone': 'remote-repository', 'new-repository': 'empty-repository', 'private-snapshot': 'filtered-workspace' } as const)[mode], initialized, revision: before.id, manifestSha256: before.manifestSha256, persistence: 'not-applied', ...(namespace ? { scope: ({ 'host-mirror': 'host-draft', 'remote-clone': 'clone-draft', 'new-repository': 'new-draft', 'private-snapshot': 'draft' } as const)[mode] } : {}) }
const assertCurrent = async (): Promise<void> => {
assertActive(signal); await assertPolicyCurrent(policy)
const current = await activeIdentity(root)
if (current?.id !== before.id || current.manifestSha256 !== before.manifestSha256) throw new Error('Sandbox Git revision changed during execution or approval')
}
return { state, assertCurrent, close,
async capture(signal) {
const inventory = await scanRepository(target, signal)
if (hash(JSON.stringify(inventory.files)) === manifest.contentHash) return null
return storeRepository(root, target, policy, inventory, signal, manifest.origin)
},
async publish(candidate) {
await assertCurrent()
await manifestAt(root, candidate, policy.workspaceRoot)
await publishIdentity(root, candidate)
state.revision = candidate.id; state.manifestSha256 = candidate.manifestSha256; state.persistence = 'saved'
}
}
} catch (error) { await close(); throw error }
}
/** Trusted control-plane copy; validates every immutable source before publishing it. */
export async function copySandboxGitRepository(workspace: string, namespace: string, destination: string): Promise<{ identity: GitIdentity; manifest: GitManifest }> {
const root = await prepareRoot(workspace, namespace), lockPath = join(root, 'repository.lock'), lock = await fs.open(lockPath, 'wx', 0o600)
try {
await lock.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }) + '\n')
const identity = await activeIdentity(root)
if (!identity) throw new Error('Sandbox Git has no saved repository')
const manifest = await manifestAt(root, identity, workspace)
await fs.mkdir(destination, { recursive: true })
await fs.mkdir(join(destination, 'objects'), { recursive: true }); await fs.mkdir(join(destination, 'refs'), { recursive: true })
for (const file of manifest.files) {
const bytes = await regular(join(root, 'objects', identity.id, 'files', file.path))
if (bytes.length !== file.bytes || hash(bytes) !== file.sha256) throw new Error('Sandbox Git saved repository was changed')
await fs.mkdir(dirname(join(destination, file.path)), { recursive: true })
await fs.writeFile(join(destination, file.path), bytes, { flag: 'wx', mode: 0o600 })
}
return { identity, manifest }
} finally { await lock.close(); await fs.unlink(lockPath) }
}
export async function replaceSandboxGitRepository(policy: SandboxEffectivePolicy, namespace: string, source: string, expected?: GitIdentity, origin?: 'host' | 'remote' | 'empty'): Promise<GitIdentity> {
const root = await prepareRoot(policy.workspaceRoot, namespace), lockPath = join(root, 'repository.lock')
if (origin && !await fs.lstat(join(root, 'host-sealed')).catch(() => null)) {
if (await fs.lstat(lockPath).catch(() => null)) throw new Error('Sandbox Git is in use')
await sealWorkspace(root)
await fs.writeFile(join(root, 'host-sealed'), '1\n', { flag: 'wx' })
}
const lock = await fs.open(lockPath, 'wx', 0o600)
try {
await lock.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }) + '\n')
const current = await activeIdentity(root)
if (expected && (current?.id !== expected.id || current.manifestSha256 !== expected.manifestSha256)) throw new Error('Sandbox Git revision changed during control-plane operation')
const next = await storeRepository(root, source, policy, await scanRepository(source), undefined, origin)
await assertPolicyCurrent(policy)
await publishIdentity(root, next)
return next
} finally { await lock.close(); await fs.unlink(lockPath) }
}
export async function sandboxGitIdentity(workspace: string, namespace: string): Promise<GitIdentity | null> {
return activeIdentity(await prepareRoot(workspace, namespace))
}
export async function sandboxGitState(workspaceInput: string): Promise<Record<string, unknown>> {
const workspace = await fs.realpath(workspaceInput), root = await prepareRoot(workspace), identity = await activeIdentity(root)
if (!identity) return { mode: 'private-snapshot', state: 'missing' }
const manifest = await manifestAt(root, identity, workspace)
return { mode: 'private-snapshot', baseline: 'filtered-workspace', state: 'saved', ...identity, policyFingerprint: manifest.policyFingerprint, files: manifest.files.length, bytes: manifest.bytes, createdAt: manifest.createdAt }
}
/** Archive only the active reference; immutable revisions remain available for inspection. */
export async function resetSandboxGit(workspaceInput: string, namespace?: string): Promise<void> {
const workspace = await fs.realpath(workspaceInput), root = await prepareRoot(workspace, namespace), lockPath = join(root, 'repository.lock')
const lock = await fs.open(lockPath, 'wx', 0o600)
try {
await lock.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }) + '\n')
if (await activeIdentity(root)) await fs.rename(join(root, 'active.json'), join(root, `archived-${randomUUID()}.json`))
} finally { await lock.close(); await fs.unlink(lockPath) }
}
export async function recoverSandboxGitLock(workspace: string, namespace: string): Promise<void> {
const path = join(repositoryRoot(workspace, namespace), 'repository.lock')
if (!await fs.lstat(path).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error })) return
const bytes = await regular(path, 4096), value = JSON.parse(bytes.toString('utf8')) as { pid: number }
if (!Number.isSafeInteger(value.pid) || value.pid < 1) throw new Error('Sandbox Git lock identity is invalid')
try { process.kill(value.pid, 0); throw new Error('Sandbox Git lock still has a live owner') }
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error }
if (!(await regular(path, 4096)).equals(bytes)) throw new Error('Sandbox Git lock changed during recovery')
await fs.unlink(path)
}