feat: add managed SSH execution spaces
Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI. Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts. Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
function detectElfArchitecture(buffer) {
|
||||
if (
|
||||
buffer.length < 20 ||
|
||||
buffer[0] !== 0x7f ||
|
||||
buffer.toString('ascii', 1, 4) !== 'ELF' ||
|
||||
![1, 2].includes(buffer[5])
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const machine =
|
||||
buffer[5] === 2
|
||||
? buffer.readUInt16BE(18)
|
||||
: buffer.readUInt16LE(18)
|
||||
if (machine === 62) {
|
||||
return 'x64'
|
||||
}
|
||||
if (machine === 183) {
|
||||
return 'arm64'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function detectBinaryArchitecture(buffer) {
|
||||
if (
|
||||
buffer.length >= 64 &&
|
||||
buffer[0] === 0x4d &&
|
||||
buffer[1] === 0x5a
|
||||
) {
|
||||
const peOffset = buffer.readUInt32LE(0x3c)
|
||||
if (
|
||||
peOffset + 6 <= buffer.length &&
|
||||
buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
|
||||
) {
|
||||
const machine = buffer.readUInt16LE(peOffset + 4)
|
||||
if (machine === 0x8664) {
|
||||
return 'x64'
|
||||
}
|
||||
if (machine === 0xaa64) {
|
||||
return 'arm64'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elfArchitecture = detectElfArchitecture(buffer)
|
||||
if (elfArchitecture) {
|
||||
return elfArchitecture
|
||||
}
|
||||
|
||||
if (buffer.length >= 8) {
|
||||
const littleMagic = buffer.readUInt32LE(0)
|
||||
const bigMagic = buffer.readUInt32BE(0)
|
||||
const cpuType =
|
||||
littleMagic === 0xfeedfacf
|
||||
? buffer.readUInt32LE(4)
|
||||
: bigMagic === 0xfeedfacf
|
||||
? buffer.readUInt32BE(4)
|
||||
: undefined
|
||||
if (cpuType === 0x01000007) {
|
||||
return 'x64'
|
||||
}
|
||||
if (cpuType === 0x0100000c) {
|
||||
return 'arm64'
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectBinaryArchitecture,
|
||||
detectElfArchitecture
|
||||
}
|
||||
+248
-92
@@ -11,6 +11,11 @@ const {
|
||||
writeFileSync
|
||||
} = require('node:fs')
|
||||
const { dirname, join, parse, resolve } = require('node:path')
|
||||
const {
|
||||
supportedArchitectures,
|
||||
targetName,
|
||||
verifyLockedBundle
|
||||
} = require('./agent-bundle.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -29,19 +34,6 @@ const portablePath = process.env.GOODBUDDY_OUT_DIR
|
||||
const markerName = '.goodbuddy-portable.json'
|
||||
const portableLocales = new Set(['zh-CN.pak', 'en-US.pak'])
|
||||
|
||||
if (process.platform !== 'win32' || process.arch !== 'x64') {
|
||||
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
|
||||
}
|
||||
if (
|
||||
portablePath === parse(portablePath).root ||
|
||||
portablePath === root ||
|
||||
portablePath === outputRoot
|
||||
) {
|
||||
throw new Error(
|
||||
`拒绝使用不安全的输出目录:${portablePath}`
|
||||
)
|
||||
}
|
||||
|
||||
function assertOutputUnlocked(directory) {
|
||||
if (!existsSync(directory)) {
|
||||
return
|
||||
@@ -138,10 +130,106 @@ function pruneLocales(directory) {
|
||||
}
|
||||
}
|
||||
|
||||
function portableRequiredPaths(directory) {
|
||||
function parseRequiredAgentArchitectures(value) {
|
||||
if (value === undefined) {
|
||||
return []
|
||||
}
|
||||
const architectures = value
|
||||
.split(',')
|
||||
.map((architecture) => architecture.trim())
|
||||
if (
|
||||
architectures.length === 0 ||
|
||||
architectures.some(
|
||||
(architecture) =>
|
||||
!supportedArchitectures.includes(architecture)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'GOODBUDDY_PORTABLE_REQUIRE_AGENT_ARCHS 仅支持逗号分隔的 x64 和 arm64'
|
||||
)
|
||||
}
|
||||
return [...new Set(architectures)]
|
||||
}
|
||||
|
||||
function discoverLocalAgentResources(projectRoot = root) {
|
||||
return supportedArchitectures.filter((architecture) =>
|
||||
statSync(
|
||||
join(
|
||||
projectRoot,
|
||||
'.agent-resources',
|
||||
targetName(architecture)
|
||||
),
|
||||
{ throwIfNoEntry: false }
|
||||
)?.isDirectory()
|
||||
)
|
||||
}
|
||||
|
||||
function embedPortableAgentResources(directory, options = {}) {
|
||||
const projectRoot = options.projectRoot ?? root
|
||||
const requiredArchitectures =
|
||||
options.requiredArchitectures ??
|
||||
parseRequiredAgentArchitectures(
|
||||
process.env.GOODBUDDY_PORTABLE_REQUIRE_AGENT_ARCHS
|
||||
)
|
||||
const verifyBundle =
|
||||
options.verifyLockedBundle ?? verifyLockedBundle
|
||||
const availableArchitectures =
|
||||
discoverLocalAgentResources(projectRoot)
|
||||
const missingArchitectures = requiredArchitectures.filter(
|
||||
(architecture) =>
|
||||
!availableArchitectures.includes(architecture)
|
||||
)
|
||||
if (missingArchitectures.length > 0) {
|
||||
throw new Error(
|
||||
`Portable 构建缺少要求的 Agent 资源:${missingArchitectures
|
||||
.map(targetName)
|
||||
.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const architecture of availableArchitectures) {
|
||||
verifyBundle(
|
||||
join(
|
||||
projectRoot,
|
||||
'.agent-resources',
|
||||
targetName(architecture)
|
||||
),
|
||||
architecture,
|
||||
{ projectRoot }
|
||||
)
|
||||
}
|
||||
for (const architecture of availableArchitectures) {
|
||||
const target = targetName(architecture)
|
||||
const source = join(
|
||||
projectRoot,
|
||||
'.agent-resources',
|
||||
target
|
||||
)
|
||||
const destination = join(
|
||||
directory,
|
||||
'resources',
|
||||
'agents',
|
||||
target
|
||||
)
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
cpSync(source, destination, {
|
||||
recursive: true,
|
||||
force: true
|
||||
})
|
||||
}
|
||||
return availableArchitectures
|
||||
}
|
||||
|
||||
function portableRequiredPaths(
|
||||
directory,
|
||||
agentArchitectures = []
|
||||
) {
|
||||
return [
|
||||
join(directory, 'GoodBuddy.exe'),
|
||||
join(directory, 'resources', 'app.asar'),
|
||||
join(directory, 'resources', 'agent-runtime-lock.json'),
|
||||
join(directory, 'resources', 'remote-runtime-lock.json'),
|
||||
join(directory, 'resources', 'agent-release-keys.json'),
|
||||
join(directory, 'resources', 'icon.ico'),
|
||||
join(directory, 'resources', 'tray-icon.png'),
|
||||
join(
|
||||
@@ -157,12 +245,33 @@ function portableRequiredPaths(directory) {
|
||||
'runtimes',
|
||||
'continue',
|
||||
'package.json'
|
||||
)
|
||||
),
|
||||
...agentArchitectures.flatMap((architecture) => {
|
||||
const agentDirectory = join(
|
||||
directory,
|
||||
'resources',
|
||||
'agents',
|
||||
targetName(architecture)
|
||||
)
|
||||
return [
|
||||
join(agentDirectory, 'manifest.json'),
|
||||
join(agentDirectory, 'manifest.sig'),
|
||||
join(agentDirectory, 'goodbuddy-agent'),
|
||||
join(agentDirectory, 'node'),
|
||||
join(agentDirectory, 'lib', 'agent.cjs')
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
function assertPortableOutput(directory) {
|
||||
const requiredPaths = portableRequiredPaths(directory)
|
||||
function assertPortableOutput(
|
||||
directory,
|
||||
agentArchitectures = []
|
||||
) {
|
||||
const requiredPaths = portableRequiredPaths(
|
||||
directory,
|
||||
agentArchitectures
|
||||
)
|
||||
const missing = requiredPaths.filter(
|
||||
(filePath) => !statSync(filePath, { throwIfNoEntry: false })?.isFile()
|
||||
)
|
||||
@@ -219,7 +328,11 @@ function writePortableMarker(directory) {
|
||||
)
|
||||
}
|
||||
|
||||
function replacePortableOutput(source, destination) {
|
||||
function replacePortableOutput(
|
||||
source,
|
||||
destination,
|
||||
agentArchitectures = []
|
||||
) {
|
||||
const replacement = `${destination}.replacement-${process.pid}`
|
||||
const backup = `${destination}.previous-${process.pid}`
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
@@ -228,7 +341,7 @@ function replacePortableOutput(source, destination) {
|
||||
copyDirectoryContents(source, replacement)
|
||||
try {
|
||||
writePortableMarker(replacement)
|
||||
assertPortableOutput(replacement)
|
||||
assertPortableOutput(replacement, agentArchitectures)
|
||||
|
||||
if (!existsSync(destination)) {
|
||||
renameSync(replacement, destination)
|
||||
@@ -250,7 +363,7 @@ function replacePortableOutput(source, destination) {
|
||||
}
|
||||
renameSync(preservedData, destinationData)
|
||||
}
|
||||
assertPortableOutput(destination)
|
||||
assertPortableOutput(destination, agentArchitectures)
|
||||
} catch (error) {
|
||||
try {
|
||||
const movedData = join(destination, 'data')
|
||||
@@ -279,36 +392,80 @@ function replacePortableOutput(source, destination) {
|
||||
}
|
||||
}
|
||||
|
||||
const electronDist = ensureElectronRuntime()
|
||||
mkdirSync(outputRoot, { recursive: true })
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
function main() {
|
||||
if (process.platform !== 'win32' || process.arch !== 'x64') {
|
||||
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
|
||||
}
|
||||
if (
|
||||
portablePath === parse(portablePath).root ||
|
||||
portablePath === root ||
|
||||
portablePath === outputRoot
|
||||
) {
|
||||
throw new Error(
|
||||
`拒绝使用不安全的输出目录:${portablePath}`
|
||||
)
|
||||
}
|
||||
const requiredAgentArchitectures =
|
||||
parseRequiredAgentArchitectures(
|
||||
process.env.GOODBUDDY_PORTABLE_REQUIRE_AGENT_ARCHS
|
||||
)
|
||||
const electronDist = ensureElectronRuntime()
|
||||
mkdirSync(outputRoot, { recursive: true })
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
|
||||
for (const [label, script, args] of [
|
||||
[
|
||||
'Node 类型检查',
|
||||
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
|
||||
['--noEmit', '-p', 'tsconfig.node.json']
|
||||
],
|
||||
[
|
||||
'Renderer 类型检查',
|
||||
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
|
||||
['--noEmit', '-p', 'tsconfig.web.json']
|
||||
],
|
||||
[
|
||||
'Production bundle',
|
||||
join(
|
||||
root,
|
||||
'node_modules',
|
||||
'electron-vite',
|
||||
'bin',
|
||||
'electron-vite.js'
|
||||
),
|
||||
['build']
|
||||
]
|
||||
]) {
|
||||
const buildResult = spawnSync(
|
||||
for (const [label, script, args] of [
|
||||
[
|
||||
'Node 类型检查',
|
||||
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
|
||||
['--noEmit', '-p', 'tsconfig.node.json']
|
||||
],
|
||||
[
|
||||
'Renderer 类型检查',
|
||||
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
|
||||
['--noEmit', '-p', 'tsconfig.web.json']
|
||||
],
|
||||
[
|
||||
'Production bundle',
|
||||
join(
|
||||
root,
|
||||
'node_modules',
|
||||
'electron-vite',
|
||||
'bin',
|
||||
'electron-vite.js'
|
||||
),
|
||||
['build']
|
||||
]
|
||||
]) {
|
||||
const buildResult = spawnSync(
|
||||
process.execPath,
|
||||
[script, ...args],
|
||||
{
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: 'inherit',
|
||||
windowsHide: true
|
||||
}
|
||||
)
|
||||
if (buildResult.error) {
|
||||
throw buildResult.error
|
||||
}
|
||||
if (buildResult.status !== 0) {
|
||||
throw new Error(
|
||||
`${label}失败(code ${buildResult.status ?? 1})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[script, ...args],
|
||||
[
|
||||
join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
'--dir',
|
||||
'--x64',
|
||||
`--config.directories.output=${stagingRoot}`,
|
||||
`--config.electronDist=${electronDist}`
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
@@ -317,55 +474,54 @@ for (const [label, script, args] of [
|
||||
windowsHide: true
|
||||
}
|
||||
)
|
||||
if (buildResult.error) {
|
||||
throw buildResult.error
|
||||
|
||||
if (result.error) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw result.error
|
||||
}
|
||||
if (buildResult.status !== 0) {
|
||||
if (result.status !== 0) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw new Error(
|
||||
`${label}失败(code ${buildResult.status ?? 1})`
|
||||
`Electron Builder 构建失败(code ${result.status ?? 1})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
'--dir',
|
||||
'--x64',
|
||||
`--config.directories.output=${stagingRoot}`,
|
||||
`--config.electronDist=${electronDist}`
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: 'inherit',
|
||||
windowsHide: true
|
||||
if (!statSync(unpackedPath, {
|
||||
throwIfNoEntry: false
|
||||
})?.isDirectory()) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw new Error('Electron Builder 未生成 portable 目录')
|
||||
}
|
||||
try {
|
||||
pruneLocales(unpackedPath)
|
||||
const embeddedAgentArchitectures =
|
||||
embedPortableAgentResources(unpackedPath, {
|
||||
requiredArchitectures: requiredAgentArchitectures
|
||||
})
|
||||
assertPortableOutput(
|
||||
unpackedPath,
|
||||
embeddedAgentArchitectures
|
||||
)
|
||||
replacePortableOutput(
|
||||
unpackedPath,
|
||||
portablePath,
|
||||
embeddedAgentArchitectures
|
||||
)
|
||||
} finally {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
}
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw result.error
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw new Error(
|
||||
`Electron Builder 构建失败(code ${result.status ?? 1})`
|
||||
)
|
||||
}
|
||||
if (!statSync(unpackedPath, { throwIfNoEntry: false })?.isDirectory()) {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
throw new Error('Electron Builder 未生成 portable 目录')
|
||||
}
|
||||
try {
|
||||
pruneLocales(unpackedPath)
|
||||
assertPortableOutput(unpackedPath)
|
||||
replacePortableOutput(unpackedPath, portablePath)
|
||||
} finally {
|
||||
rmSync(stagingRoot, { recursive: true, force: true })
|
||||
console.log(`Portable 目录构建完成:${portablePath}`)
|
||||
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
|
||||
}
|
||||
|
||||
console.log(`Portable 目录构建完成:${portablePath}`)
|
||||
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
|
||||
module.exports = {
|
||||
assertPortableOutput,
|
||||
discoverLocalAgentResources,
|
||||
embedPortableAgentResources,
|
||||
parseRequiredAgentArchitectures,
|
||||
portableRequiredPaths
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
|
||||
+142
-49
@@ -5,6 +5,7 @@ const {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
closeSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
@@ -35,6 +36,16 @@ const {
|
||||
} = require('@electron/asar')
|
||||
const { Zip, ZipDeflate } = require('fflate')
|
||||
const { sha256File } = require('./file-hash.cjs')
|
||||
const {
|
||||
detectBinaryArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
const {
|
||||
verifyAgentBundleMatrix
|
||||
} = require('./agent-bundle.cjs')
|
||||
const {
|
||||
supportedArchitectures: remoteRuntimeArchitectures,
|
||||
verifyBundleDirectory: verifyRemoteRuntimeBundle
|
||||
} = require('./remote-runtime-bundle.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -45,6 +56,11 @@ const packageLock = JSON.parse(
|
||||
)
|
||||
const productName = packageJson.build?.productName ?? packageJson.name
|
||||
const releaseRoot = join(root, 'dist', 'release')
|
||||
const releaseBuilderConfig = join(
|
||||
root,
|
||||
'build',
|
||||
'electron-builder.release.cjs'
|
||||
)
|
||||
const manifestName = 'release-manifest.json'
|
||||
const portableMarkerName = '.goodbuddy-portable.json'
|
||||
const harnessHostEntry =
|
||||
@@ -328,6 +344,8 @@ function buildElectronBuilderArguments(options, outputDirectory) {
|
||||
)]
|
||||
const builderArguments = [
|
||||
join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
'--config',
|
||||
releaseBuilderConfig,
|
||||
definition.builderFlag,
|
||||
...builderFormats,
|
||||
`--${options.arch}`,
|
||||
@@ -382,59 +400,101 @@ function electronBuilderEnvironment(
|
||||
return builderEnvironment
|
||||
}
|
||||
|
||||
function detectBinaryArchitecture(buffer) {
|
||||
if (
|
||||
buffer.length >= 64 &&
|
||||
buffer[0] === 0x4d &&
|
||||
buffer[1] === 0x5a
|
||||
) {
|
||||
const peOffset = buffer.readUInt32LE(0x3c)
|
||||
if (
|
||||
peOffset + 6 <= buffer.length &&
|
||||
buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
|
||||
) {
|
||||
const machine = buffer.readUInt16LE(peOffset + 4)
|
||||
if (machine === 0x8664) {
|
||||
return 'x64'
|
||||
function verifyReleaseAgentResources(
|
||||
projectRoot = root,
|
||||
verifyBundleMatrix = verifyAgentBundleMatrix
|
||||
) {
|
||||
verifyBundleMatrix(
|
||||
join(projectRoot, '.agent-resources'),
|
||||
{ projectRoot }
|
||||
)
|
||||
}
|
||||
|
||||
function verifyRemoteRuntimeBundleMatrix(
|
||||
resourcesRoot,
|
||||
options = {}
|
||||
) {
|
||||
const projectRoot = options.projectRoot ?? root
|
||||
const verifyBundleDirectory =
|
||||
options.verifyBundleDirectory ?? verifyRemoteRuntimeBundle
|
||||
const registry = readJsonFile(
|
||||
options.registryPath ??
|
||||
join(projectRoot, 'resources', 'agent-release-keys.json'),
|
||||
'Remote Runtime trusted key registry'
|
||||
)
|
||||
const lock = readJsonFile(
|
||||
options.lockPath ??
|
||||
join(projectRoot, 'remote-runtime-lock.json'),
|
||||
'Remote Runtime lock'
|
||||
)
|
||||
for (const architecture of remoteRuntimeArchitectures) {
|
||||
const runtimeRoot = join(
|
||||
resourcesRoot,
|
||||
`linux-${architecture}`,
|
||||
'opencode'
|
||||
)
|
||||
let rootStat
|
||||
try {
|
||||
rootStat = lstatSync(runtimeRoot)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle root is missing for ${architecture}: ${runtimeRoot}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle root must be a real directory for ${architecture}: ${runtimeRoot}`
|
||||
)
|
||||
}
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(runtimeRoot, { withFileTypes: true })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle root cannot be read for ${architecture}: ${runtimeRoot}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!/^[a-f0-9]{64}$/u.test(entry.name)) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle root contains an unexpected entry for ${architecture}: ${entry.name}`
|
||||
)
|
||||
}
|
||||
if (machine === 0xaa64) {
|
||||
return 'arm64'
|
||||
const entryStat = lstatSync(join(runtimeRoot, entry.name))
|
||||
if (entryStat.isSymbolicLink() || !entryStat.isDirectory()) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle entry must be a real directory for ${architecture}: ${entry.name}`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (entries.length !== 1) {
|
||||
throw new Error(
|
||||
`Remote Runtime bundle root must contain exactly one digest directory for ${architecture}; found ${entries.length}`
|
||||
)
|
||||
}
|
||||
verifyBundleDirectory(
|
||||
join(runtimeRoot, entries[0].name),
|
||||
{
|
||||
projectRoot,
|
||||
architecture,
|
||||
registry,
|
||||
lock,
|
||||
verificationEnvironment: 'production'
|
||||
}
|
||||
)
|
||||
}
|
||||
if (
|
||||
buffer.length >= 20 &&
|
||||
buffer[0] === 0x7f &&
|
||||
buffer.toString('ascii', 1, 4) === 'ELF'
|
||||
) {
|
||||
const machine =
|
||||
buffer[5] === 2
|
||||
? buffer.readUInt16BE(18)
|
||||
: buffer.readUInt16LE(18)
|
||||
if (machine === 62) {
|
||||
return 'x64'
|
||||
}
|
||||
if (machine === 183) {
|
||||
return 'arm64'
|
||||
}
|
||||
}
|
||||
if (buffer.length >= 8) {
|
||||
const littleMagic = buffer.readUInt32LE(0)
|
||||
const bigMagic = buffer.readUInt32BE(0)
|
||||
const cpuType =
|
||||
littleMagic === 0xfeedfacf
|
||||
? buffer.readUInt32LE(4)
|
||||
: bigMagic === 0xfeedfacf
|
||||
? buffer.readUInt32BE(4)
|
||||
: undefined
|
||||
if (cpuType === 0x01000007) {
|
||||
return 'x64'
|
||||
}
|
||||
if (cpuType === 0x0100000c) {
|
||||
return 'arm64'
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function verifyReleaseRemoteRuntimeResources(
|
||||
projectRoot = root,
|
||||
verifyBundleMatrix = verifyRemoteRuntimeBundleMatrix
|
||||
) {
|
||||
verifyBundleMatrix(
|
||||
join(projectRoot, '.remote-runtime-resources'),
|
||||
{ projectRoot }
|
||||
)
|
||||
}
|
||||
|
||||
function binaryArchitecture(filePath) {
|
||||
@@ -1086,6 +1146,34 @@ function verifyUnpackedOutput(directory, options) {
|
||||
assertFile(applicationExecutable, '应用主程序')
|
||||
assertFile(join(resources, 'app.asar'), '应用 ASAR')
|
||||
assertFile(join(resources, 'release-notes.json'), '版本更新说明')
|
||||
assertFile(
|
||||
join(resources, 'agent-release-keys.json'),
|
||||
'Agent 受信任签名密钥'
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'agent-runtime-lock.json'),
|
||||
'Agent Runtime 锁定清单'
|
||||
)
|
||||
verifyAgentBundleMatrix(join(resources, 'agents'), {
|
||||
projectRoot: root,
|
||||
registryPath: join(resources, 'agent-release-keys.json'),
|
||||
lockPath: join(resources, 'agent-runtime-lock.json')
|
||||
})
|
||||
assertFile(
|
||||
join(resources, 'remote-runtime-lock.json'),
|
||||
'Remote Runtime 锁定清单'
|
||||
)
|
||||
verifyRemoteRuntimeBundleMatrix(
|
||||
join(resources, 'remote-runtimes'),
|
||||
{
|
||||
projectRoot: root,
|
||||
registryPath: join(
|
||||
resources,
|
||||
'agent-release-keys.json'
|
||||
),
|
||||
lockPath: join(resources, 'remote-runtime-lock.json')
|
||||
}
|
||||
)
|
||||
assertFile(runtimeExecutable, 'OpenCode Runtime')
|
||||
assertFile(
|
||||
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
|
||||
@@ -1655,6 +1743,8 @@ async function main(argv = process.argv.slice(2)) {
|
||||
)
|
||||
}
|
||||
|
||||
verifyReleaseAgentResources()
|
||||
verifyReleaseRemoteRuntimeResources()
|
||||
rmSync(stagingDirectory, { recursive: true, force: true })
|
||||
let cleanupTargetDependencies = () => undefined
|
||||
try {
|
||||
@@ -1724,6 +1814,9 @@ module.exports = {
|
||||
verifyArtifacts,
|
||||
verifyArtifactSignature,
|
||||
verifyPortableZip,
|
||||
verifyReleaseAgentResources,
|
||||
verifyReleaseRemoteRuntimeResources,
|
||||
verifyRemoteRuntimeBundleMatrix,
|
||||
writeManifest
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const packageJson = require('../package.json')
|
||||
const {
|
||||
supportedArchitectures,
|
||||
targetName
|
||||
} = require('./agent-bundle.cjs')
|
||||
|
||||
const remoteRuntimeResources = supportedArchitectures.map(
|
||||
(architecture) => {
|
||||
const target = targetName(architecture)
|
||||
return {
|
||||
from: `.remote-runtime-resources/${target}`,
|
||||
to: `remote-runtimes/${target}`,
|
||||
filter: ['**/*']
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const agentResources = [
|
||||
...supportedArchitectures.map((architecture) => {
|
||||
const target = targetName(architecture)
|
||||
return {
|
||||
from: `.agent-resources/${target}`,
|
||||
to: `agents/${target}`,
|
||||
filter: ['**/*']
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
...packageJson.build,
|
||||
extraResources: [
|
||||
...packageJson.build.extraResources,
|
||||
...agentResources,
|
||||
...remoteRuntimeResources
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user