fix: restore cross-platform release packaging
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
The unpublished 0.11.3 candidate could not validate the DeepSeek Harness node-pty prebuilds on every native target, and catalog signing could misread large compound Agent payloads. Release packaging now verifies the rc.8 native layout and writes deterministic stored .gbagent archives with bounded central-directory, mode, CRC, SHA-256, and signature checks. The 0.11.4 recovery also pins reviewed dependency lifecycle scripts, packages the omitted ONNX Runtime license, and carries the approved unpublished notes forward without changing the immutable failed tags or adding a LoongArch package. Release note: 修复跨平台桌面包和大文件 Agent 包的发行校验失败,并将未发布的 0.11.3 内容顺延至 0.11.4。
This commit is contained in:
@@ -1171,6 +1171,7 @@ module.exports = {
|
||||
createAgentArchive,
|
||||
createManifest,
|
||||
detectElfArchitecture,
|
||||
expectedManifestMode,
|
||||
importAgentArchive,
|
||||
lockedRuntimeInput,
|
||||
preflightProductionSigningKey,
|
||||
|
||||
+216
-113
@@ -6,18 +6,14 @@ const {
|
||||
} = require('node:crypto')
|
||||
const {
|
||||
closeSync,
|
||||
fstatSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
statSync,
|
||||
writeFileSync
|
||||
} = require('node:fs')
|
||||
const { basename, resolve } = require('node:path')
|
||||
const {
|
||||
Unzip,
|
||||
UnzipInflate,
|
||||
UnzipPassThrough
|
||||
} = require('fflate')
|
||||
const { crc32 } = require('node:zlib')
|
||||
const {
|
||||
productionSigningKey,
|
||||
publicKeySpkiBase64,
|
||||
@@ -29,6 +25,11 @@ const {
|
||||
descriptorContentDigest,
|
||||
signatureDomain: packageSignatureDomain
|
||||
} = require('./agent-package.cjs')
|
||||
const {
|
||||
localFileHeaderSignature,
|
||||
readExactZipBytes,
|
||||
readZipCentralDirectory
|
||||
} = require('./zip-central-directory.cjs')
|
||||
|
||||
const root = resolve(__dirname, '..')
|
||||
const catalogName = 'agent-catalog.json'
|
||||
@@ -41,6 +42,11 @@ const semanticVersionPattern =
|
||||
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
|
||||
const sha256Pattern = /^[a-f0-9]{64}$/u
|
||||
const maximumPackageBytes = 512 * 1024 * 1024
|
||||
const maximumPackageEntries = 50_002
|
||||
const maximumPackageEntryBytes = 384 * 1024 * 1024
|
||||
const maximumExpandedPackageBytes = 1024 * 1024 * 1024
|
||||
const maximumMetadataBytes = 1024 * 1024
|
||||
const maximumCentralDirectoryBytes = 32 * 1024 * 1024
|
||||
const maximumCatalogEntries = 200
|
||||
const windowsReservedNamePattern =
|
||||
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu
|
||||
@@ -183,14 +189,6 @@ function assertPackageDescriptor(descriptor) {
|
||||
|
||||
function readPackageMetadata(archivePath, registry) {
|
||||
const archive = resolve(archivePath)
|
||||
const status = statSync(archive)
|
||||
if (
|
||||
!status.isFile() ||
|
||||
status.size <= 0 ||
|
||||
status.size > maximumPackageBytes
|
||||
) {
|
||||
throw new Error('Agent package archive is not a bounded file')
|
||||
}
|
||||
const streamed = streamPackageMetadata(archive)
|
||||
const descriptorBytes = streamed.descriptor
|
||||
const signatureBytes = streamed.signature
|
||||
@@ -225,7 +223,7 @@ function readPackageMetadata(archivePath, registry) {
|
||||
return {
|
||||
archive: expectedArchive,
|
||||
descriptor,
|
||||
size: status.size,
|
||||
size: streamed.size,
|
||||
sha256: streamed.sha256
|
||||
}
|
||||
}
|
||||
@@ -282,127 +280,232 @@ function verifyPackageFileInventory(descriptor, actualFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
function streamPackageMetadata(archivePath) {
|
||||
const handle = openSync(archivePath, 'r')
|
||||
const hash = createHash('sha256')
|
||||
const buffer = Buffer.allocUnsafe(64 * 1024)
|
||||
const values = new Map()
|
||||
const files = new Map()
|
||||
function centralDirectoryEntries(handle, archiveSize) {
|
||||
const parsed = readZipCentralDirectory(handle, archiveSize, {
|
||||
label: 'Agent package',
|
||||
minimumEntries: 1,
|
||||
maximumEntries: maximumPackageEntries,
|
||||
maximumCentralDirectoryBytes
|
||||
})
|
||||
const entries = []
|
||||
const seen = new Set()
|
||||
let fatalError
|
||||
let entries = 0
|
||||
let expandedBytes = 0
|
||||
const unzip = new Unzip((file) => {
|
||||
entries += 1
|
||||
if (entries > 50_002) {
|
||||
fatalError ??= new Error(
|
||||
'Agent package contains too many entries'
|
||||
)
|
||||
file.terminate()
|
||||
return
|
||||
for (const entry of parsed.entries) {
|
||||
if (
|
||||
entry.flags !== 0x0800 ||
|
||||
entry.compression !== 0 ||
|
||||
entry.compressedSize === 0xffffffff ||
|
||||
entry.size === 0xffffffff ||
|
||||
entry.compressedSize !== entry.size ||
|
||||
entry.size > maximumPackageEntryBytes ||
|
||||
entry.diskStart !== 0 ||
|
||||
entry.localOffset === 0xffffffff
|
||||
) {
|
||||
throw new Error('Agent package ZIP entry is unsupported')
|
||||
}
|
||||
if (!safePackagePath(file.name) || seen.has(file.name)) {
|
||||
fatalError ??= new Error(
|
||||
if (
|
||||
!safePackagePath(entry.name) ||
|
||||
seen.has(entry.name)
|
||||
) {
|
||||
throw new Error(
|
||||
'Agent package contains an unsafe or duplicate path'
|
||||
)
|
||||
file.terminate()
|
||||
return
|
||||
}
|
||||
seen.add(file.name)
|
||||
const wanted =
|
||||
file.name === 'agent-package.json' ||
|
||||
file.name === 'agent-package.sig'
|
||||
if (wanted && values.has(file.name)) {
|
||||
fatalError ??= new Error(
|
||||
'Agent package contains duplicate metadata'
|
||||
seen.add(entry.name)
|
||||
expandedBytes += entry.size
|
||||
if (expandedBytes > maximumExpandedPackageBytes) {
|
||||
throw new Error(
|
||||
'Agent package expanded payload exceeds its limit'
|
||||
)
|
||||
file.terminate()
|
||||
return
|
||||
}
|
||||
const chunks = []
|
||||
let size = 0
|
||||
const fileHash = createHash('sha256')
|
||||
file.ondata = (error, data, final) => {
|
||||
if (error) {
|
||||
fatalError ??= error
|
||||
return
|
||||
}
|
||||
size += data.byteLength
|
||||
expandedBytes += data.byteLength
|
||||
if (
|
||||
size > 384 * 1024 * 1024 ||
|
||||
expandedBytes > 1024 * 1024 * 1024
|
||||
) {
|
||||
fatalError ??= new Error(
|
||||
'Agent package expanded payload exceeds its limit'
|
||||
)
|
||||
file.terminate()
|
||||
return
|
||||
}
|
||||
fileHash.update(data)
|
||||
if (wanted) {
|
||||
if (size > 1024 * 1024) {
|
||||
fatalError ??= new Error(
|
||||
'Agent package metadata exceeds its limit'
|
||||
)
|
||||
file.terminate()
|
||||
return
|
||||
}
|
||||
chunks.push(Buffer.from(data))
|
||||
}
|
||||
if (final) {
|
||||
files.set(file.name, {
|
||||
size,
|
||||
sha256: fileHash.digest('hex')
|
||||
})
|
||||
if (wanted) {
|
||||
values.set(file.name, Buffer.concat(chunks, size))
|
||||
}
|
||||
}
|
||||
const local = readExactZipBytes(
|
||||
handle,
|
||||
30,
|
||||
entry.localOffset,
|
||||
'Agent package local file header'
|
||||
)
|
||||
const localNameLength = local.readUInt16LE(26)
|
||||
const localExtraLength = local.readUInt16LE(28)
|
||||
if (
|
||||
local.readUInt32LE(0) !== localFileHeaderSignature ||
|
||||
local.readUInt16LE(6) !== entry.flags ||
|
||||
local.readUInt16LE(8) !== entry.compression ||
|
||||
local.readUInt32LE(14) !== entry.checksum ||
|
||||
local.readUInt32LE(18) !== entry.compressedSize ||
|
||||
local.readUInt32LE(22) !== entry.size ||
|
||||
localNameLength !== entry.nameBytes.length
|
||||
) {
|
||||
throw new Error('Agent package local file header is invalid')
|
||||
}
|
||||
try {
|
||||
file.start()
|
||||
} catch (error) {
|
||||
fatalError ??= error
|
||||
file.terminate()
|
||||
const localName = readExactZipBytes(
|
||||
handle,
|
||||
localNameLength,
|
||||
entry.localOffset + 30,
|
||||
'Agent package local file name'
|
||||
)
|
||||
if (!localName.equals(entry.nameBytes)) {
|
||||
throw new Error('Agent package local file name is invalid')
|
||||
}
|
||||
})
|
||||
unzip.register(UnzipPassThrough)
|
||||
unzip.register(UnzipInflate)
|
||||
const dataOffset =
|
||||
entry.localOffset +
|
||||
30 +
|
||||
localNameLength +
|
||||
localExtraLength
|
||||
const recordEnd = dataOffset + entry.compressedSize
|
||||
if (recordEnd > parsed.centralOffset) {
|
||||
throw new Error('Agent package entry exceeds its data region')
|
||||
}
|
||||
entries.push({
|
||||
checksum: entry.checksum,
|
||||
dataOffset,
|
||||
name: entry.name,
|
||||
recordStart: entry.localOffset,
|
||||
recordEnd
|
||||
})
|
||||
}
|
||||
entries.sort(
|
||||
(left, right) => left.recordStart - right.recordStart
|
||||
)
|
||||
for (let index = 1; index < entries.length; index += 1) {
|
||||
if (entries[index].recordStart < entries[index - 1].recordEnd) {
|
||||
throw new Error('Agent package ZIP entries overlap')
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
function sameFileSnapshot(left, right) {
|
||||
return (
|
||||
left.dev === right.dev &&
|
||||
left.ino === right.ino &&
|
||||
left.size === right.size &&
|
||||
left.mtimeMs === right.mtimeMs &&
|
||||
left.ctimeMs === right.ctimeMs
|
||||
)
|
||||
}
|
||||
|
||||
function streamPackageMetadata(archivePath) {
|
||||
const handle = openSync(archivePath, 'r')
|
||||
const values = new Map()
|
||||
const files = new Map()
|
||||
try {
|
||||
while (true) {
|
||||
const initialStatus = fstatSync(handle)
|
||||
if (
|
||||
!initialStatus.isFile() ||
|
||||
initialStatus.size <= 0 ||
|
||||
initialStatus.size > maximumPackageBytes
|
||||
) {
|
||||
throw new Error(
|
||||
'Agent package archive is not a bounded file'
|
||||
)
|
||||
}
|
||||
const entries = centralDirectoryEntries(
|
||||
handle,
|
||||
initialStatus.size
|
||||
)
|
||||
const states = entries.map((entry) => {
|
||||
const wanted =
|
||||
entry.name === 'agent-package.json' ||
|
||||
entry.name === 'agent-package.sig'
|
||||
const size = entry.recordEnd - entry.dataOffset
|
||||
if (wanted && size > maximumMetadataBytes) {
|
||||
throw new Error('Agent package metadata exceeds its limit')
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
bytes: 0,
|
||||
contents: wanted
|
||||
? Buffer.allocUnsafe(size)
|
||||
: undefined,
|
||||
crc32: 0,
|
||||
hash: createHash('sha256')
|
||||
}
|
||||
})
|
||||
const archiveHash = createHash('sha256')
|
||||
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
||||
let position = 0
|
||||
let firstEntry = 0
|
||||
while (position < initialStatus.size) {
|
||||
const bytesRead = readSync(
|
||||
handle,
|
||||
buffer,
|
||||
0,
|
||||
buffer.length,
|
||||
null
|
||||
Math.min(buffer.length, initialStatus.size - position),
|
||||
position
|
||||
)
|
||||
if (bytesRead === 0) {
|
||||
unzip.push(new Uint8Array(), true)
|
||||
break
|
||||
if (bytesRead <= 0) {
|
||||
throw new Error('Agent package archive is truncated')
|
||||
}
|
||||
const chunk = Uint8Array.from(
|
||||
buffer.subarray(0, bytesRead)
|
||||
const chunk = buffer.subarray(0, bytesRead)
|
||||
const chunkEnd = position + bytesRead
|
||||
archiveHash.update(chunk)
|
||||
while (
|
||||
firstEntry < states.length &&
|
||||
states[firstEntry].recordEnd <= position
|
||||
) {
|
||||
firstEntry += 1
|
||||
}
|
||||
for (
|
||||
let index = firstEntry;
|
||||
index < states.length &&
|
||||
states[index].dataOffset < chunkEnd;
|
||||
index += 1
|
||||
) {
|
||||
const state = states[index]
|
||||
const dataStart = Math.max(position, state.dataOffset)
|
||||
const dataEnd = Math.min(chunkEnd, state.recordEnd)
|
||||
if (dataStart >= dataEnd) {
|
||||
continue
|
||||
}
|
||||
const data = chunk.subarray(
|
||||
dataStart - position,
|
||||
dataEnd - position
|
||||
)
|
||||
state.hash.update(data)
|
||||
state.crc32 = crc32(data, state.crc32)
|
||||
if (state.contents) {
|
||||
data.copy(
|
||||
state.contents,
|
||||
dataStart - state.dataOffset
|
||||
)
|
||||
}
|
||||
state.bytes += data.byteLength
|
||||
}
|
||||
position = chunkEnd
|
||||
}
|
||||
if (!sameFileSnapshot(initialStatus, fstatSync(handle))) {
|
||||
throw new Error(
|
||||
'Agent package archive changed during verification'
|
||||
)
|
||||
hash.update(chunk)
|
||||
unzip.push(chunk, false)
|
||||
if (fatalError) {
|
||||
throw fatalError
|
||||
}
|
||||
for (const state of states) {
|
||||
const size = state.recordEnd - state.dataOffset
|
||||
if (
|
||||
state.bytes !== size ||
|
||||
(state.crc32 >>> 0) !== state.checksum
|
||||
) {
|
||||
throw new Error(
|
||||
`Agent package ZIP checksum verification failed: ${state.name}`
|
||||
)
|
||||
}
|
||||
files.set(state.name, {
|
||||
size,
|
||||
sha256: state.hash.digest('hex')
|
||||
})
|
||||
if (state.contents) {
|
||||
values.set(state.name, state.contents)
|
||||
}
|
||||
}
|
||||
return {
|
||||
descriptor: values.get('agent-package.json'),
|
||||
signature: values.get('agent-package.sig'),
|
||||
files,
|
||||
sha256: archiveHash.digest('hex'),
|
||||
size: initialStatus.size
|
||||
}
|
||||
} finally {
|
||||
closeSync(handle)
|
||||
}
|
||||
if (fatalError) {
|
||||
throw fatalError
|
||||
}
|
||||
return {
|
||||
descriptor: values.get('agent-package.json'),
|
||||
signature: values.get('agent-package.sig'),
|
||||
files,
|
||||
sha256: hash.digest('hex')
|
||||
}
|
||||
}
|
||||
|
||||
function catalogEntry(metadata) {
|
||||
|
||||
+143
-60
@@ -19,9 +19,10 @@ const {
|
||||
} = require('node:fs')
|
||||
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { Zip, ZipPassThrough } = require('fflate')
|
||||
const { crc32 } = require('node:zlib')
|
||||
const {
|
||||
buildAgentBundle,
|
||||
expectedManifestMode: expectedAgentManifestMode,
|
||||
preflightProductionSigningKey,
|
||||
productionSigningKey,
|
||||
publicKeySpkiBase64,
|
||||
@@ -36,6 +37,11 @@ const {
|
||||
readRemoteRuntimeLock,
|
||||
verifyBundleDirectory: verifyRuntimeBundle
|
||||
} = require('./remote-runtime-bundle.cjs')
|
||||
const {
|
||||
centralDirectoryHeaderSignature: zipCentralHeaderSignature,
|
||||
endOfCentralDirectorySignature: zipEndSignature,
|
||||
localFileHeaderSignature: zipLocalHeaderSignature
|
||||
} = require('./zip-central-directory.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageDescriptorName = 'agent-package.json'
|
||||
@@ -44,7 +50,10 @@ const signatureDomain = Buffer.from(
|
||||
'GoodBuddy Agent Package Descriptor Signature v1\0',
|
||||
'utf8'
|
||||
)
|
||||
const archiveEpoch = new Date('1980-01-01T00:00:00.000Z')
|
||||
const zipUtf8Flag = 0x0800
|
||||
const zipVersion = 20
|
||||
const zipMadeByUnix = (3 << 8) | zipVersion
|
||||
const zipDosDate = (1 << 5) | 1
|
||||
const semanticVersionPattern =
|
||||
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
|
||||
|
||||
@@ -89,10 +98,11 @@ function listFiles(directory) {
|
||||
)
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
function fileDigests(filePath) {
|
||||
const hash = createHash('sha256')
|
||||
const handle = openSync(filePath, 'r')
|
||||
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
||||
let checksum = 0
|
||||
try {
|
||||
while (true) {
|
||||
const bytesRead = readSync(
|
||||
@@ -105,12 +115,17 @@ function sha256File(filePath) {
|
||||
if (bytesRead === 0) {
|
||||
break
|
||||
}
|
||||
hash.update(buffer.subarray(0, bytesRead))
|
||||
const chunk = buffer.subarray(0, bytesRead)
|
||||
hash.update(chunk)
|
||||
checksum = crc32(chunk, checksum)
|
||||
}
|
||||
} finally {
|
||||
closeSync(handle)
|
||||
}
|
||||
return hash.digest('hex')
|
||||
return {
|
||||
crc32: checksum >>> 0,
|
||||
sha256: hash.digest('hex')
|
||||
}
|
||||
}
|
||||
|
||||
function copyTree(source, destination) {
|
||||
@@ -122,6 +137,13 @@ function copyTree(source, destination) {
|
||||
}
|
||||
}
|
||||
|
||||
function packageFileMode(path) {
|
||||
if (path.startsWith('agent/')) {
|
||||
return expectedAgentManifestMode(path.slice('agent/'.length))
|
||||
}
|
||||
return /\/bin\/opencode$/u.test(path) ? '0755' : '0644'
|
||||
}
|
||||
|
||||
function stagePackagePayload(options) {
|
||||
const staging = options.staging
|
||||
copyTree(options.agentBundle, join(staging, 'agent'))
|
||||
@@ -157,20 +179,18 @@ function stagePackagePayload(options) {
|
||||
}
|
||||
|
||||
function payloadFiles(staging) {
|
||||
return listFiles(staging)
|
||||
const crc32ByPath = new Map()
|
||||
const files = listFiles(staging)
|
||||
.map((filePath) => {
|
||||
const path = relative(staging, filePath).split(sep).join('/')
|
||||
const size = statSync(filePath).size
|
||||
const digests = fileDigests(filePath)
|
||||
crc32ByPath.set(path, digests.crc32)
|
||||
return {
|
||||
path,
|
||||
size,
|
||||
sha256: sha256File(filePath),
|
||||
mode:
|
||||
path === 'agent/goodbuddy-agent' ||
|
||||
path === 'agent/node' ||
|
||||
/\/bin\/opencode$/u.test(path)
|
||||
? '0755'
|
||||
: '0644'
|
||||
sha256: digests.sha256,
|
||||
mode: packageFileMode(path)
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
@@ -178,18 +198,44 @@ function payloadFiles(staging) {
|
||||
file.path !== packageDescriptorName &&
|
||||
file.path !== packageSignatureName
|
||||
)
|
||||
return { crc32ByPath, files }
|
||||
}
|
||||
|
||||
function createPackageArchive(staging, output) {
|
||||
function createPackageArchive(
|
||||
staging,
|
||||
output,
|
||||
knownCrc32 = new Map()
|
||||
) {
|
||||
mkdirSync(dirname(output), { recursive: true })
|
||||
const outputHandle = openSync(output, 'w')
|
||||
let archiveError
|
||||
let completed = false
|
||||
const archive = new Zip((error, data, final) => {
|
||||
if (error) {
|
||||
archiveError ??= error
|
||||
return
|
||||
const entries = listFiles(staging).map((filePath) => {
|
||||
const name = relative(staging, filePath).split(sep).join('/')
|
||||
const nameBytes = Buffer.from(name, 'utf8')
|
||||
const size = statSync(filePath).size
|
||||
if (
|
||||
nameBytes.length === 0 ||
|
||||
nameBytes.length > 0xffff ||
|
||||
size > 0xffffffff
|
||||
) {
|
||||
throw new Error(`Agent package ZIP entry is too large: ${name}`)
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
name,
|
||||
nameBytes,
|
||||
size,
|
||||
crc32:
|
||||
knownCrc32.get(name) ?? fileDigests(filePath).crc32,
|
||||
mode: packageFileMode(name),
|
||||
offset: 0
|
||||
}
|
||||
})
|
||||
if (entries.length === 0 || entries.length > 0xffff) {
|
||||
throw new Error('Agent package ZIP entry count is invalid')
|
||||
}
|
||||
const outputHandle = openSync(output, 'w')
|
||||
let outputOffset = 0
|
||||
const archiveHash = createHash('sha256')
|
||||
const writeBytes = (data) => {
|
||||
let offset = 0
|
||||
while (offset < data.byteLength) {
|
||||
const written = writeSync(
|
||||
@@ -199,69 +245,101 @@ function createPackageArchive(staging, output) {
|
||||
data.byteLength - offset
|
||||
)
|
||||
if (written <= 0) {
|
||||
archiveError ??= new Error(
|
||||
'Agent package archive write made no progress'
|
||||
)
|
||||
return
|
||||
throw new Error('Agent package archive write made no progress')
|
||||
}
|
||||
offset += written
|
||||
}
|
||||
completed ||= final
|
||||
})
|
||||
archiveHash.update(data)
|
||||
outputOffset += data.byteLength
|
||||
}
|
||||
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
||||
try {
|
||||
for (const filePath of listFiles(staging)) {
|
||||
const name = relative(staging, filePath).split(sep).join('/')
|
||||
const input = new ZipPassThrough(name)
|
||||
input.mtime = archiveEpoch
|
||||
archive.add(input)
|
||||
const inputHandle = openSync(filePath, 'r')
|
||||
const fileSize = statSync(filePath).size
|
||||
for (const entry of entries) {
|
||||
entry.offset = outputOffset
|
||||
const header = Buffer.alloc(30)
|
||||
header.writeUInt32LE(zipLocalHeaderSignature, 0)
|
||||
header.writeUInt16LE(zipVersion, 4)
|
||||
header.writeUInt16LE(zipUtf8Flag, 6)
|
||||
header.writeUInt16LE(0, 8)
|
||||
header.writeUInt16LE(0, 10)
|
||||
header.writeUInt16LE(zipDosDate, 12)
|
||||
header.writeUInt32LE(entry.crc32, 14)
|
||||
header.writeUInt32LE(entry.size, 18)
|
||||
header.writeUInt32LE(entry.size, 22)
|
||||
header.writeUInt16LE(entry.nameBytes.length, 26)
|
||||
header.writeUInt16LE(0, 28)
|
||||
writeBytes(header)
|
||||
writeBytes(entry.nameBytes)
|
||||
|
||||
const inputHandle = openSync(entry.filePath, 'r')
|
||||
try {
|
||||
let position = 0
|
||||
if (fileSize === 0) {
|
||||
input.push(new Uint8Array(), true)
|
||||
}
|
||||
while (position < fileSize) {
|
||||
while (position < entry.size) {
|
||||
const bytesRead = readSync(
|
||||
inputHandle,
|
||||
buffer,
|
||||
0,
|
||||
buffer.length,
|
||||
Math.min(buffer.length, entry.size - position),
|
||||
null
|
||||
)
|
||||
if (bytesRead <= 0) {
|
||||
throw new Error(
|
||||
`Agent package source read made no progress: ${name}`
|
||||
`Agent package source read made no progress: ${entry.name}`
|
||||
)
|
||||
}
|
||||
position += bytesRead
|
||||
input.push(
|
||||
Uint8Array.from(buffer.subarray(0, bytesRead)),
|
||||
position === fileSize
|
||||
)
|
||||
writeBytes(buffer.subarray(0, bytesRead))
|
||||
}
|
||||
} finally {
|
||||
closeSync(inputHandle)
|
||||
}
|
||||
if (archiveError) {
|
||||
throw archiveError
|
||||
}
|
||||
}
|
||||
archive.end()
|
||||
if (archiveError) {
|
||||
throw archiveError
|
||||
}
|
||||
if (!completed) {
|
||||
throw new Error('Agent package archive did not finish')
|
||||
|
||||
const centralOffset = outputOffset
|
||||
for (const entry of entries) {
|
||||
const record = Buffer.alloc(46)
|
||||
record.writeUInt32LE(zipCentralHeaderSignature, 0)
|
||||
record.writeUInt16LE(zipMadeByUnix, 4)
|
||||
record.writeUInt16LE(zipVersion, 6)
|
||||
record.writeUInt16LE(zipUtf8Flag, 8)
|
||||
record.writeUInt16LE(0, 10)
|
||||
record.writeUInt16LE(0, 12)
|
||||
record.writeUInt16LE(zipDosDate, 14)
|
||||
record.writeUInt32LE(entry.crc32, 16)
|
||||
record.writeUInt32LE(entry.size, 20)
|
||||
record.writeUInt32LE(entry.size, 24)
|
||||
record.writeUInt16LE(entry.nameBytes.length, 28)
|
||||
record.writeUInt16LE(0, 30)
|
||||
record.writeUInt16LE(0, 32)
|
||||
record.writeUInt16LE(0, 34)
|
||||
record.writeUInt16LE(0, 36)
|
||||
const unixMode =
|
||||
0o100000 | Number.parseInt(entry.mode, 8)
|
||||
record.writeUInt32LE((unixMode << 16) >>> 0, 38)
|
||||
record.writeUInt32LE(entry.offset, 42)
|
||||
writeBytes(record)
|
||||
writeBytes(entry.nameBytes)
|
||||
}
|
||||
|
||||
const centralSize = outputOffset - centralOffset
|
||||
const end = Buffer.alloc(22)
|
||||
end.writeUInt32LE(zipEndSignature, 0)
|
||||
end.writeUInt16LE(0, 4)
|
||||
end.writeUInt16LE(0, 6)
|
||||
end.writeUInt16LE(entries.length, 8)
|
||||
end.writeUInt16LE(entries.length, 10)
|
||||
end.writeUInt32LE(centralSize, 12)
|
||||
end.writeUInt32LE(centralOffset, 16)
|
||||
end.writeUInt16LE(0, 20)
|
||||
writeBytes(end)
|
||||
fsyncSync(outputHandle)
|
||||
} catch (error) {
|
||||
archive.terminate()
|
||||
throw error
|
||||
} finally {
|
||||
closeSync(outputHandle)
|
||||
}
|
||||
return {
|
||||
size: outputOffset,
|
||||
sha256: archiveHash.digest('hex')
|
||||
}
|
||||
}
|
||||
|
||||
function assembleAgentPackage(options) {
|
||||
@@ -345,6 +423,7 @@ function assembleAgentPackage(options) {
|
||||
agentBundle: resolve(options.agentBundle),
|
||||
runtimeBundle: runtime.bundleDirectory
|
||||
})
|
||||
const payload = payloadFiles(staging)
|
||||
const initial = {
|
||||
format: 'goodbuddy-agent-package',
|
||||
formatVersion: 1,
|
||||
@@ -364,7 +443,7 @@ function assembleAgentPackage(options) {
|
||||
protocol: runtime.manifest.protocol
|
||||
},
|
||||
contentDigest: `sha256:${'0'.repeat(64)}`,
|
||||
files: payloadFiles(staging)
|
||||
files: payload.files
|
||||
}
|
||||
const descriptor = {
|
||||
...initial,
|
||||
@@ -382,12 +461,16 @@ function assembleAgentPackage(options) {
|
||||
`${signature.toString('base64')}\n`,
|
||||
'utf8'
|
||||
)
|
||||
createPackageArchive(staging, options.output)
|
||||
const archive = createPackageArchive(
|
||||
staging,
|
||||
options.output,
|
||||
payload.crc32ByPath
|
||||
)
|
||||
return {
|
||||
descriptor,
|
||||
archive: basename(options.output),
|
||||
size: statSync(options.output).size,
|
||||
sha256: sha256File(options.output)
|
||||
size: archive.size,
|
||||
sha256: archive.sha256
|
||||
}
|
||||
} finally {
|
||||
rmSync(staging, { recursive: true, force: true })
|
||||
|
||||
+43
-93
@@ -5,6 +5,7 @@ const {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
closeSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
@@ -38,6 +39,9 @@ const { sha256File } = require('./file-hash.cjs')
|
||||
const {
|
||||
detectBinaryArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
const {
|
||||
readZipCentralDirectory
|
||||
} = require('./zip-central-directory.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -522,9 +526,10 @@ function targetHarnessPaths(options) {
|
||||
koffiPackage,
|
||||
koffiBinary,
|
||||
nodePtyBinary:
|
||||
options.platform === 'linux'
|
||||
? 'build/Release/pty.node'
|
||||
: `prebuilds/${platformName}-${options.arch}/pty.node`,
|
||||
`prebuilds/${platformName}-${options.arch}/` +
|
||||
(options.platform === 'windows'
|
||||
? 'conpty.node'
|
||||
: 'pty.node'),
|
||||
nodePtyDirectory: `${platformName}-${options.arch}`
|
||||
}
|
||||
}
|
||||
@@ -1000,7 +1005,7 @@ function verifyHarnessPackage(
|
||||
'DeepSeek Harness Canvas'
|
||||
)
|
||||
|
||||
if (options.platform === 'darwin') {
|
||||
if (options.platform === 'macos') {
|
||||
const helper = join(
|
||||
unpackedRoot,
|
||||
'node_modules',
|
||||
@@ -1274,98 +1279,42 @@ async function createPortableZip(
|
||||
}
|
||||
|
||||
function readZipEntryNames(filePath) {
|
||||
const fileSize = statSync(filePath).size
|
||||
if (fileSize < 22) {
|
||||
throw new Error('Portable ZIP 缺少中央目录')
|
||||
}
|
||||
const endChunkSize = Math.min(fileSize, 65_557)
|
||||
const endChunkStart = fileSize - endChunkSize
|
||||
const endChunk = readChunk(
|
||||
filePath,
|
||||
endChunkSize,
|
||||
endChunkStart
|
||||
)
|
||||
let endOffset = -1
|
||||
for (let index = endChunk.length - 22; index >= 0; index -= 1) {
|
||||
if (
|
||||
endChunk.readUInt32LE(index) === 0x06054b50 &&
|
||||
index + 22 + endChunk.readUInt16LE(index + 20) ===
|
||||
endChunk.length
|
||||
) {
|
||||
endOffset = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if (endOffset < 0) {
|
||||
throw new Error('Portable ZIP 缺少中央目录')
|
||||
}
|
||||
const diskNumber = endChunk.readUInt16LE(endOffset + 4)
|
||||
const centralDisk = endChunk.readUInt16LE(endOffset + 6)
|
||||
const diskEntryCount = endChunk.readUInt16LE(endOffset + 8)
|
||||
const entryCount = endChunk.readUInt16LE(endOffset + 10)
|
||||
const centralSize = endChunk.readUInt32LE(endOffset + 12)
|
||||
const centralOffset = endChunk.readUInt32LE(endOffset + 16)
|
||||
const absoluteEndOffset = endChunkStart + endOffset
|
||||
if (
|
||||
diskNumber !== 0 ||
|
||||
centralDisk !== 0 ||
|
||||
diskEntryCount !== entryCount ||
|
||||
entryCount === 0xffff ||
|
||||
centralSize === 0xffffffff ||
|
||||
centralOffset === 0xffffffff ||
|
||||
entryCount < portableRequiredFiles.length + 1 ||
|
||||
entryCount > maxPortableZipEntries ||
|
||||
centralSize < 46 ||
|
||||
centralSize > maxPortableCentralDirectoryBytes ||
|
||||
centralOffset + centralSize !== absoluteEndOffset
|
||||
) {
|
||||
throw new Error('Portable ZIP 中央目录无效')
|
||||
}
|
||||
const centralDirectory = readChunk(
|
||||
filePath,
|
||||
centralSize,
|
||||
centralOffset
|
||||
)
|
||||
const names = []
|
||||
let offset = 0
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
if (
|
||||
offset + 46 > centralDirectory.length ||
|
||||
centralDirectory.readUInt32LE(offset) !== 0x02014b50
|
||||
) {
|
||||
throw new Error('Portable ZIP 中央目录条目无效')
|
||||
}
|
||||
const nameLength = centralDirectory.readUInt16LE(offset + 28)
|
||||
const extraLength = centralDirectory.readUInt16LE(offset + 30)
|
||||
const commentLength = centralDirectory.readUInt16LE(offset + 32)
|
||||
const entryLength = 46 + nameLength + extraLength + commentLength
|
||||
if (offset + entryLength > centralDirectory.length) {
|
||||
throw new Error('Portable ZIP 中央目录条目越界')
|
||||
}
|
||||
const name = centralDirectory
|
||||
.subarray(offset + 46, offset + 46 + nameLength)
|
||||
.toString(
|
||||
centralDirectory.readUInt16LE(offset + 8) & 0x0800
|
||||
? 'utf8'
|
||||
: 'latin1'
|
||||
const handle = openSync(filePath, 'r')
|
||||
try {
|
||||
let parsed
|
||||
try {
|
||||
parsed = readZipCentralDirectory(
|
||||
handle,
|
||||
fstatSync(handle).size,
|
||||
{
|
||||
label: 'Portable ZIP',
|
||||
minimumEntries: portableRequiredFiles.length + 1,
|
||||
maximumEntries: maxPortableZipEntries,
|
||||
maximumCentralDirectoryBytes:
|
||||
maxPortableCentralDirectoryBytes
|
||||
}
|
||||
)
|
||||
.replaceAll('\\', '/')
|
||||
if (
|
||||
!name ||
|
||||
name.startsWith('/') ||
|
||||
/^[a-z]:\//iu.test(name) ||
|
||||
name.includes('\0') ||
|
||||
name.split('/').some((part) => part === '..')
|
||||
) {
|
||||
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
|
||||
} catch (error) {
|
||||
throw new Error('Portable ZIP 中央目录无效', {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
names.push(name)
|
||||
offset += entryLength
|
||||
return parsed.entries.map((entry) => {
|
||||
const name = entry.name.replaceAll('\\', '/')
|
||||
if (
|
||||
!name ||
|
||||
name.startsWith('/') ||
|
||||
/^[a-z]:\//iu.test(name) ||
|
||||
name.includes('\0') ||
|
||||
name.split('/').some((part) => part === '..')
|
||||
) {
|
||||
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
|
||||
}
|
||||
return name
|
||||
})
|
||||
} finally {
|
||||
closeSync(handle)
|
||||
}
|
||||
if (offset !== centralDirectory.length) {
|
||||
throw new Error('Portable ZIP 中央目录数量不一致')
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
function verifyPortableZip(filePath) {
|
||||
@@ -1698,6 +1647,7 @@ module.exports = {
|
||||
replaceOutput,
|
||||
stageTargetRuntimeDependencies,
|
||||
targetRuntimePackageNames,
|
||||
targetHarnessPaths,
|
||||
verifyHarnessPackage,
|
||||
verifyArchiveIntegrity,
|
||||
verifyUnpackedOutput,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
const { readSync } = require('node:fs')
|
||||
|
||||
const localFileHeaderSignature = 0x04034b50
|
||||
const centralDirectoryHeaderSignature = 0x02014b50
|
||||
const endOfCentralDirectorySignature = 0x06054b50
|
||||
|
||||
function readExactZipBytes(handle, length, position, label) {
|
||||
const output = Buffer.allocUnsafe(length)
|
||||
let offset = 0
|
||||
while (offset < length) {
|
||||
const bytesRead = readSync(
|
||||
handle,
|
||||
output,
|
||||
offset,
|
||||
length - offset,
|
||||
position + offset
|
||||
)
|
||||
if (bytesRead <= 0) {
|
||||
throw new Error(`${label} is truncated`)
|
||||
}
|
||||
offset += bytesRead
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function readZipCentralDirectory(handle, archiveSize, limits) {
|
||||
if (archiveSize < 22) {
|
||||
throw new Error(`${limits.label} central directory is missing`)
|
||||
}
|
||||
const endSize = Math.min(archiveSize, 65_557)
|
||||
const endStart = archiveSize - endSize
|
||||
const end = readExactZipBytes(
|
||||
handle,
|
||||
endSize,
|
||||
endStart,
|
||||
`${limits.label} end record`
|
||||
)
|
||||
let endOffset = -1
|
||||
for (let index = end.length - 22; index >= 0; index -= 1) {
|
||||
if (
|
||||
end.readUInt32LE(index) === endOfCentralDirectorySignature &&
|
||||
index + 22 + end.readUInt16LE(index + 20) === end.length
|
||||
) {
|
||||
endOffset = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if (endOffset < 0) {
|
||||
throw new Error(`${limits.label} central directory is missing`)
|
||||
}
|
||||
const diskNumber = end.readUInt16LE(endOffset + 4)
|
||||
const centralDisk = end.readUInt16LE(endOffset + 6)
|
||||
const diskEntryCount = end.readUInt16LE(endOffset + 8)
|
||||
const entryCount = end.readUInt16LE(endOffset + 10)
|
||||
const centralSize = end.readUInt32LE(endOffset + 12)
|
||||
const centralOffset = end.readUInt32LE(endOffset + 16)
|
||||
const absoluteEndOffset = endStart + endOffset
|
||||
if (
|
||||
diskNumber !== 0 ||
|
||||
centralDisk !== 0 ||
|
||||
diskEntryCount !== entryCount ||
|
||||
entryCount === 0 ||
|
||||
entryCount === 0xffff ||
|
||||
entryCount < limits.minimumEntries ||
|
||||
entryCount > limits.maximumEntries ||
|
||||
centralSize === 0xffffffff ||
|
||||
centralSize < 46 ||
|
||||
centralSize > limits.maximumCentralDirectoryBytes ||
|
||||
centralOffset === 0xffffffff ||
|
||||
centralOffset + centralSize !== absoluteEndOffset
|
||||
) {
|
||||
throw new Error(`${limits.label} central directory is invalid`)
|
||||
}
|
||||
const central = readExactZipBytes(
|
||||
handle,
|
||||
centralSize,
|
||||
centralOffset,
|
||||
`${limits.label} central directory`
|
||||
)
|
||||
const entries = []
|
||||
let offset = 0
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
if (
|
||||
offset + 46 > central.length ||
|
||||
central.readUInt32LE(offset) !==
|
||||
centralDirectoryHeaderSignature
|
||||
) {
|
||||
throw new Error(
|
||||
`${limits.label} central directory entry is invalid`
|
||||
)
|
||||
}
|
||||
const nameLength = central.readUInt16LE(offset + 28)
|
||||
const extraLength = central.readUInt16LE(offset + 30)
|
||||
const commentLength = central.readUInt16LE(offset + 32)
|
||||
const entryLength =
|
||||
46 + nameLength + extraLength + commentLength
|
||||
if (offset + entryLength > central.length) {
|
||||
throw new Error(
|
||||
`${limits.label} central directory entry exceeds its bounds`
|
||||
)
|
||||
}
|
||||
const flags = central.readUInt16LE(offset + 8)
|
||||
const nameBytes = central.subarray(
|
||||
offset + 46,
|
||||
offset + 46 + nameLength
|
||||
)
|
||||
const name = new TextDecoder(
|
||||
flags & 0x0800 ? 'utf-8' : 'latin1',
|
||||
{ fatal: true }
|
||||
).decode(nameBytes)
|
||||
entries.push({
|
||||
name,
|
||||
nameBytes: Buffer.from(nameBytes),
|
||||
flags,
|
||||
compression: central.readUInt16LE(offset + 10),
|
||||
checksum: central.readUInt32LE(offset + 16),
|
||||
compressedSize: central.readUInt32LE(offset + 20),
|
||||
size: central.readUInt32LE(offset + 24),
|
||||
diskStart: central.readUInt16LE(offset + 34),
|
||||
localOffset: central.readUInt32LE(offset + 42)
|
||||
})
|
||||
offset += entryLength
|
||||
}
|
||||
if (offset !== central.length) {
|
||||
throw new Error(
|
||||
`${limits.label} central directory count is invalid`
|
||||
)
|
||||
}
|
||||
return {
|
||||
centralOffset,
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
centralDirectoryHeaderSignature,
|
||||
endOfCentralDirectorySignature,
|
||||
localFileHeaderSignature,
|
||||
readExactZipBytes,
|
||||
readZipCentralDirectory
|
||||
}
|
||||
Reference in New Issue
Block a user