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,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
|
||||
Reference in new issue
Block a user