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

+134
View File
@@ -0,0 +1,134 @@
'use strict'
const fs = require('node:fs')
const { randomUUID, createHash } = require('node:crypto')
const prefix = '\x1eYCODE_WORKSPACE_READ_V1 '
let selected = false
function error(code, message) { return Object.assign(new Error(message), { code }) }
function frame(operation, args, id) { return '\n' + prefix + JSON.stringify({ version: 1, requestId: id, operation, args }) + '\n' }
function send(operation, args, id) {
const bytes = Buffer.from(frame(operation, args, id))
let offset = 0
while (offset < bytes.length) offset += fs.writeSync(1, bytes, offset, bytes.length - offset)
}
function unpack(line, id) {
let response
try { response = JSON.parse(line) } catch { throw error('EPROTO', 'Invalid file bridge reply') }
if (response.version !== 1 || response.requestId !== id || typeof response.ok !== 'boolean') throw error('EPROTO', 'Unexpected file bridge reply')
if (!response.ok) throw error(response.error?.code || 'EACCES', response.error?.message || 'Workspace read denied')
return response.value
}
function claim() {
if (selected) throw error('EBUSY', 'Choose one bridge client and mode per process; stdin belongs to that client')
selected = true
}
function decode(part, initial, offset) {
if (!part || part.snapshotId !== initial.snapshotId || part.sha256 !== initial.sha256 || part.size !== initial.size || part.offset !== offset || typeof part.data !== 'string') throw error('EPROTO', 'Inconsistent read snapshot')
const bytes = Buffer.from(part.data, 'base64')
if (bytes.length > 16384 || !bytes.length && offset < initial.size || offset + bytes.length > initial.size || part.eof !== (offset + bytes.length === initial.size)) throw error('EPROTO', 'Invalid read chunk')
return bytes
}
function complete(chunks, initial, encoding) {
const bytes = Buffer.concat(chunks)
if (bytes.length !== initial.size || createHash('sha256').update(bytes).digest('hex') !== initial.sha256) throw error('EPROTO', 'Read snapshot digest mismatch')
return encoding ? bytes.toString(encoding) : bytes
}
function connectSync() {
claim()
let buffered = Buffer.alloc(0), closed = false
function call(operation, args, id = randomUUID()) {
if (closed) throw error('EPIPE', 'File bridge client closed')
send(operation, args, id)
for (;;) {
const end = buffered.indexOf(10)
if (end >= 0) {
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
return unpack(line, id)
}
if (buffered.length > 32768) throw error('EPROTO', 'File bridge reply exceeds limit')
const bytes = Buffer.alloc(32768), length = fs.readSync(0, bytes, 0, bytes.length, null)
if (!length) { closed = true; throw error('EPIPE', 'File bridge disconnected') }
buffered = Buffer.concat([buffered, bytes.subarray(0, length)])
}
}
return {
call,
readFileSync(path, encoding) {
const initial = call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
statSync: path => call('workspace.stat', { path }),
readdirSync: path => call('workspace.readdir', { path }).entries,
querySync: requestId => call('operation.query', { requestId }),
close() { closed = true }
}
}
function connect() {
claim()
const waiting = new Map(), queue = []
let buffered = Buffer.alloc(0), closed = false, failure
function shutdown(reason) {
if (closed) return
closed = true; failure = reason
process.stdin.off('data', receive); process.stdin.off('end', ended); process.stdin.off('error', failed); process.stdin.pause()
process.stdin.destroy()
for (const item of [...waiting.values(), ...queue]) item.reject(reason)
waiting.clear(); queue.length = 0
}
function flush() {
while (!closed && waiting.size < 8 && queue.length) {
const item = queue.shift(); waiting.set(item.id, item)
try { send(item.operation, item.args, item.id) } catch (reason) { shutdown(reason) }
}
if (waiting.size) { process.stdin.ref?.(); process.stdin.resume() }
else { process.stdin.pause(); process.stdin.unref?.() }
}
function receive(bytes) {
buffered = Buffer.concat([buffered, bytes])
for (;;) {
const end = buffered.indexOf(10)
if (end < 0) { if (buffered.length > 32768) shutdown(error('EPROTO', 'File bridge reply exceeds limit')); return }
const line = buffered.subarray(0, end).toString('utf8'); buffered = buffered.subarray(end + 1)
let id
try { id = JSON.parse(line).requestId } catch { shutdown(error('EPROTO', 'Invalid file bridge reply')); return }
const item = waiting.get(id)
if (!item) { shutdown(error('EPROTO', 'Unexpected file bridge reply')); return }
waiting.delete(id)
try { item.resolve(unpack(line, id)) } catch (reason) { item.reject(reason) }
flush()
}
}
const ended = () => shutdown(error('EPIPE', 'File bridge disconnected'))
const failed = reason => shutdown(reason)
process.stdin.on('data', receive); process.stdin.on('end', ended); process.stdin.on('error', failed); process.stdin.pause()
function call(operation, args, id = randomUUID()) {
if (closed) return Promise.reject(failure || error('EPIPE', 'File bridge client closed'))
if (queue.length + waiting.size >= 32) return Promise.reject(error('EBUSY', 'File bridge client queue full'))
return new Promise((resolve, reject) => { queue.push({ id, operation, args, resolve, reject }); flush() })
}
return {
call,
async readFile(path, encoding) {
const initial = await call('workspace.readFile', { path }), chunks = []
try {
let part = initial, offset = 0
for (;;) {
const bytes = decode(part, initial, offset); chunks.push(bytes); offset += bytes.length
if (part.eof) return complete(chunks, initial, encoding)
part = await call('workspace.readChunk', { snapshotId: initial.snapshotId, offset })
}
} finally { await call('workspace.closeRead', { snapshotId: initial.snapshotId }) }
},
stat: path => call('workspace.stat', { path }),
readdir: async path => (await call('workspace.readdir', { path })).entries,
query: requestId => call('operation.query', { requestId }),
close() { shutdown(error('EPIPE', 'File bridge client closed')) }
}
}
module.exports = { connect, connectSync }
@@ -0,0 +1,70 @@
'use strict'
// Explicit preload for synchronous subprocess capture in Windows AppContainers.
// Every subprocess still uses Node's own spawn and inherits the existing token/Job.
const fs = require('node:fs')
const path = require('node:path')
const cp = require('node:child_process')
const { syncBuiltinESMExports } = require('node:module')
const originals = { spawnSync: cp.spawnSync, execFileSync: cp.execFileSync, execSync: cp.execSync }
const maximum = 16 * 1024 * 1024
function captured(method, file, args, options, spawn) {
if (options.stdio !== undefined && options.stdio !== 'pipe') return method()
const limit = options.maxBuffer === undefined ? 1024 * 1024 : options.maxBuffer
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maximum) throw new RangeError('Sandbox process adapter maxBuffer must be between 0 and 16 MiB')
const input = options.input === undefined ? Buffer.alloc(0) : typeof options.input === 'string'
? Buffer.from(options.input, options.encoding && options.encoding !== 'buffer' ? options.encoding : 'utf8')
: ArrayBuffer.isView(options.input) ? Buffer.from(options.input.buffer, options.input.byteOffset, options.input.byteLength) : null
if (!input) throw new TypeError('Sandbox process adapter input must be a string, Buffer or ArrayBuffer view')
if (input.length > maximum) throw new RangeError('Sandbox process adapter input exceeds 16 MiB')
const directory = fs.mkdtempSync(path.join(process.env.TEMP, 'ycode-process-'))
const files = ['stdin', 'stdout', 'stderr'].map(name => path.join(directory, name)), handles = []
const encode = bytes => options.encoding && options.encoding !== 'buffer' ? bytes.toString(options.encoding) : bytes
let answer, failure
try {
fs.writeFileSync(files[0], input, { flag: 'wx', mode: 0o600 })
handles.push(fs.openSync(files[0], 'r'))
handles.push(fs.openSync(files[1], 'wx+'))
handles.push(fs.openSync(files[2], 'wx+'))
const forwarded = { ...options, stdio: handles }; delete forwarded.input
try { answer = method(forwarded) } catch (error) { failure = error }
const read = index => {
const buffer = Buffer.alloc(Math.min(fs.fstatSync(handles[index]).size, limit + 1))
let length = 0
while (length < buffer.length) { const count = fs.readSync(handles[index], buffer, length, buffer.length - length, length); if (!count) break; length += count }
return { bytes: buffer.subarray(0, Math.min(length, limit)), overflow: length > limit }
}
const out = read(1), err = read(2), stdout = encode(out.bytes), stderr = encode(err.bytes)
if (out.overflow || err.overflow) failure = Object.assign(new Error('Sandbox subprocess output exceeds maxBuffer'), { code: 'ENOBUFS', syscall: 'spawnSync', path: file, spawnargs: args })
if (spawn) {
if (failure && !answer) throw failure
return { ...answer, stdout, stderr, output: [null, stdout, stderr], ...(failure ? { error: failure } : {}) }
}
if (failure) {
failure.stdout = stdout; failure.stderr = stderr; failure.output = [null, stdout, stderr]
throw failure
}
return stdout
} finally {
for (const handle of handles) try { fs.closeSync(handle) } catch {}
for (const file of files) try { fs.unlinkSync(file) } catch {}
try { fs.rmdirSync(directory) } catch {}
}
}
function argumentsOf(args, options) {
return Array.isArray(args) ? { args, options: options || {} } : { args: [], options: args == null ? options || {} : args }
}
function spawnSync(file, args, options) {
const normalized = argumentsOf(args, options)
return captured(forwarded => originals.spawnSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, true)
}
function execFileSync(file, args, options) {
const normalized = argumentsOf(args, options)
return captured(forwarded => originals.execFileSync(file, normalized.args, forwarded || normalized.options), file, normalized.args, normalized.options, false)
}
function execSync(command, options = {}) {
return captured(forwarded => originals.execSync(command, forwarded || options), command, [], options, false)
}
if (process.env.YCODE_SANDBOX !== 'appcontainer') throw new Error('The subprocess adapter is for the isolated YCode Node runtime')
Object.assign(cp, { spawnSync, execFileSync, execSync })
syncBuiltinESMExports()
module.exports = { spawnSync, execFileSync, execSync }
@@ -0,0 +1,23 @@
"""Keep private Windows temporary directories accessible to this AppContainer.
Python 3.13+ gives mode 0o700 an explicit user/admin DACL on Windows. That
omits the AppContainer SID. Inside our stage, inherit the native private parent
DACL instead. Kernel permissions and the Job remain the security boundary.
"""
import os
if os.name == "nt" and os.environ.get("YCODE_SANDBOX") == "appcontainer":
_mkdir = os.mkdir
_stage = os.path.normcase(os.path.abspath(os.environ["YCODE_SANDBOX_ROOT"]))
def _private_mkdir(path, mode=0o777, *, dir_fd=None):
if mode == 0o700 and dir_fd is None:
target = os.path.normcase(os.path.abspath(os.fsdecode(path)))
try:
if os.path.commonpath((_stage, target)) == _stage:
mode = 0o777
except ValueError:
pass
return _mkdir(path, mode, dir_fd=dir_fd)
os.mkdir = _private_mkdir