Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation. Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release. Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded. Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
354 lines
12 KiB
YAML
354 lines
12 KiB
YAML
name: GoodBuddy Agents
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
pull_request:
|
|
paths:
|
|
- '.github/workflows/agents.yml'
|
|
- 'agent-runtime-lock.json'
|
|
- 'build/agent-bundle.cjs'
|
|
- 'build/agent-package.cjs'
|
|
- 'build/agent-ci-bundle.cjs'
|
|
- 'build/agent-build-target.cjs'
|
|
- 'build/binary-architecture.cjs'
|
|
- 'build/file-hash.cjs'
|
|
- 'build/remote-runtime-bundle.cjs'
|
|
- 'build/signing-key-preflight.cjs'
|
|
- 'package-lock.json'
|
|
- 'package.json'
|
|
- 'resources/agent-release-keys.json'
|
|
- 'remote-runtime-lock.json'
|
|
- 'src/agent-daemon/**'
|
|
- 'src/shared/**'
|
|
- 'tests/agent-bundle.test.ts'
|
|
- 'tests/agent-darwin-build.test.ts'
|
|
- 'tests/agent-package.test.ts'
|
|
- 'tests/agents-workflow.test.ts'
|
|
- 'tests/remote-runtime-bundle.test.ts'
|
|
push:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- '.github/workflows/agents.yml'
|
|
- 'agent-runtime-lock.json'
|
|
- 'build/agent-bundle.cjs'
|
|
- 'build/agent-package.cjs'
|
|
- 'build/agent-ci-bundle.cjs'
|
|
- 'build/agent-build-target.cjs'
|
|
- 'build/binary-architecture.cjs'
|
|
- 'build/file-hash.cjs'
|
|
- 'build/remote-runtime-bundle.cjs'
|
|
- 'build/signing-key-preflight.cjs'
|
|
- 'package-lock.json'
|
|
- 'package.json'
|
|
- 'resources/agent-release-keys.json'
|
|
- 'remote-runtime-lock.json'
|
|
- 'src/agent-daemon/**'
|
|
- 'src/shared/**'
|
|
- 'tests/agent-bundle.test.ts'
|
|
- 'tests/agent-darwin-build.test.ts'
|
|
- 'tests/agent-package.test.ts'
|
|
- 'tests/agents-workflow.test.ts'
|
|
- 'tests/remote-runtime-bundle.test.ts'
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: agents-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
validate:
|
|
name: Validate Agent source
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 15
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24.19.0
|
|
cache: npm
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Validate Agent source and workflow
|
|
run: |
|
|
npm test -- \
|
|
tests/agent-bundle.test.ts \
|
|
tests/agent-darwin-build.test.ts \
|
|
tests/agent-package.test.ts \
|
|
tests/remote-runtime-bundle.test.ts \
|
|
tests/agents-workflow.test.ts
|
|
npm run typecheck
|
|
|
|
build:
|
|
name: Build ${{ matrix.platform }} ${{ matrix.arch }} Agent
|
|
needs: validate
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: linux
|
|
arch: x64
|
|
runner: ubuntu-24.04
|
|
- platform: linux
|
|
arch: arm64
|
|
runner: ubuntu-24.04-arm
|
|
- platform: darwin
|
|
arch: arm64
|
|
runner: macos-15
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: 24.19.0
|
|
cache: npm
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Verify native runner target
|
|
run: node -e "if (process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Agent CI requires the native target')"
|
|
|
|
- name: Resolve locked official Node runtime
|
|
id: runtime
|
|
shell: bash
|
|
env:
|
|
AGENT_ARCH: ${{ matrix.arch }}
|
|
AGENT_PLATFORM: ${{ matrix.platform }}
|
|
run: |
|
|
set -euo pipefail
|
|
node - <<'NODE'
|
|
const { appendFileSync } = require('node:fs')
|
|
const { lock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
|
|
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
|
|
)
|
|
const target = lock.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
|
|
if (
|
|
!target ||
|
|
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(target.archive) ||
|
|
!/^[a-f0-9]{64}$/u.test(target.sha256)
|
|
) {
|
|
throw new Error('Locked Agent Node runtime metadata is invalid')
|
|
}
|
|
const url = new URL(target.archive, lock.node.source)
|
|
if (
|
|
url.protocol !== 'https:' ||
|
|
url.hostname !== 'nodejs.org' ||
|
|
url.username ||
|
|
url.password ||
|
|
url.search ||
|
|
url.hash
|
|
) {
|
|
throw new Error('Agent Node runtime must use the official HTTPS origin')
|
|
}
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
[
|
|
`archive=${target.archive}`,
|
|
`sha256=${target.sha256}`,
|
|
`url=${url.href}`,
|
|
''
|
|
].join('\n')
|
|
)
|
|
NODE
|
|
|
|
- name: Cache verified Node runtime input
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ${{ runner.temp }}/goodbuddy-agent-node
|
|
key: goodbuddy-agent-ci-node-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
|
|
|
|
- name: Acquire and verify locked Node runtime
|
|
shell: bash
|
|
env:
|
|
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
|
|
NODE_SHA256: ${{ steps.runtime.outputs.sha256 }}
|
|
NODE_URL: ${{ steps.runtime.outputs.url }}
|
|
run: |
|
|
set -euo pipefail
|
|
directory="$RUNNER_TEMP/goodbuddy-agent-node"
|
|
archive="$directory/$NODE_ARCHIVE"
|
|
mkdir -p "$directory"
|
|
if [[ ! -f "$archive" ]]; then
|
|
rm -f "$archive.partial"
|
|
curl --proto '=https' --tlsv1.2 \
|
|
--fail --silent --show-error --location \
|
|
--retry 3 --retry-all-errors \
|
|
--connect-timeout 15 --max-time 300 \
|
|
--max-filesize 104857600 \
|
|
"$NODE_URL" --output "$archive.partial"
|
|
mv "$archive.partial" "$archive"
|
|
fi
|
|
node - "$archive" "$NODE_SHA256" <<'NODE'
|
|
const { createHash } = require('node:crypto')
|
|
const { readFileSync } = require('node:fs')
|
|
const [archive, expected] = process.argv.slice(2)
|
|
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
|
|
throw new Error('Node runtime archive integrity mismatch')
|
|
}
|
|
NODE
|
|
|
|
- name: Resolve locked OpenCode Runtime
|
|
id: opencode
|
|
shell: bash
|
|
env:
|
|
AGENT_ARCH: ${{ matrix.arch }}
|
|
AGENT_PLATFORM: ${{ matrix.platform }}
|
|
run: |
|
|
set -euo pipefail
|
|
node - <<'NODE'
|
|
const { appendFileSync } = require('node:fs')
|
|
const { createHash: hash } = require('node:crypto')
|
|
const { runtimeLock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
|
|
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
|
|
)
|
|
const runtime = runtimeLock.runtimes?.opencode
|
|
const key = process.env.AGENT_PLATFORM === 'linux'
|
|
? process.env.AGENT_ARCH
|
|
: `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
|
|
const target = runtime?.targets?.[key]
|
|
if (
|
|
!runtime ||
|
|
!target ||
|
|
!/^[a-z0-9][a-z0-9._-]*$/u.test(target.package) ||
|
|
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(runtime.version) ||
|
|
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(target.integrity)
|
|
) {
|
|
throw new Error('Locked OpenCode Runtime metadata is invalid')
|
|
}
|
|
const archive = `${target.package}-${runtime.version}.tgz`
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
[
|
|
`package=${target.package}`,
|
|
`version=${runtime.version}`,
|
|
`integrity=${target.integrity}`,
|
|
`archive=${archive}`,
|
|
`cache-key=${hash('sha256').update(target.integrity).digest('hex')}`,
|
|
''
|
|
].join('\n')
|
|
)
|
|
NODE
|
|
|
|
- name: Cache verified OpenCode Runtime input
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ${{ runner.temp }}/goodbuddy-opencode-runtime
|
|
key: goodbuddy-agent-ci-opencode-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
|
|
|
|
- name: Acquire and verify locked OpenCode Runtime
|
|
shell: bash
|
|
env:
|
|
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
|
|
OPENCODE_INTEGRITY: ${{ steps.opencode.outputs.integrity }}
|
|
OPENCODE_PACKAGE: ${{ steps.opencode.outputs.package }}
|
|
OPENCODE_VERSION: ${{ steps.opencode.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
directory="$RUNNER_TEMP/goodbuddy-opencode-runtime"
|
|
archive="$directory/$OPENCODE_ARCHIVE"
|
|
mkdir -p "$directory"
|
|
if [[ ! -f "$archive" ]]; then
|
|
npm pack \
|
|
--ignore-scripts \
|
|
--registry https://registry.npmjs.org/ \
|
|
--pack-destination "$directory" \
|
|
"$OPENCODE_PACKAGE@$OPENCODE_VERSION"
|
|
fi
|
|
node - "$archive" "$OPENCODE_INTEGRITY" <<'NODE'
|
|
const { createHash } = require('node:crypto')
|
|
const { readFileSync, statSync } = require('node:fs')
|
|
const [archive, expected] = process.argv.slice(2)
|
|
const size = statSync(archive).size
|
|
if (size <= 0 || size > 536870912) {
|
|
throw new Error('OpenCode Runtime archive exceeds its safety limit')
|
|
}
|
|
const actual = `sha512-${createHash('sha512')
|
|
.update(readFileSync(archive))
|
|
.digest('base64')}`
|
|
if (actual !== expected) {
|
|
throw new Error('OpenCode Runtime archive integrity mismatch')
|
|
}
|
|
NODE
|
|
|
|
- name: Run native Agent regressions
|
|
if: matrix.platform == 'linux'
|
|
run: |
|
|
npm test -- \
|
|
src/agent-daemon/model-bridge.test.ts \
|
|
src/agent-daemon/lifecycle.test.ts
|
|
|
|
- name: Run native macOS process regressions
|
|
if: matrix.platform == 'darwin'
|
|
run: npm test -- src/agent-daemon/darwin-process-identity.test.ts
|
|
|
|
- name: Build and verify ephemeral signed compound Agent package twice
|
|
id: agent
|
|
shell: bash
|
|
env:
|
|
AGENT_ARCH: ${{ matrix.arch }}
|
|
AGENT_PLATFORM: ${{ matrix.platform }}
|
|
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
|
|
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
|
|
run: |
|
|
set -euo pipefail
|
|
directory="$RUNNER_TEMP/goodbuddy-agent-ci"
|
|
rm -rf "$directory"
|
|
mkdir -p "$directory"
|
|
agent_version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
|
|
node build/agent-ci-bundle.cjs \
|
|
--platform "$AGENT_PLATFORM" \
|
|
--arch "$AGENT_ARCH" \
|
|
--node-archive "$RUNNER_TEMP/goodbuddy-agent-node/$NODE_ARCHIVE" \
|
|
--opencode-archive "$RUNNER_TEMP/goodbuddy-opencode-runtime/$OPENCODE_ARCHIVE" \
|
|
--output-directory "$directory/bundle" \
|
|
--archive "$directory/goodbuddy-agent-$agent_version-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" |
|
|
tee "$directory/result.json"
|
|
node - <<'NODE'
|
|
const { appendFileSync, readFileSync } = require('node:fs')
|
|
const result = JSON.parse(
|
|
readFileSync(
|
|
`${process.env.RUNNER_TEMP}/goodbuddy-agent-ci/result.json`,
|
|
'utf8'
|
|
)
|
|
)
|
|
if (
|
|
result.architecture !== process.env.AGENT_ARCH ||
|
|
result.platform !== process.env.AGENT_PLATFORM ||
|
|
!/^[a-f0-9]{64}$/u.test(result.archiveSha256)
|
|
) {
|
|
throw new Error('Agent CI result is invalid')
|
|
}
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
`sha256=${result.archiveSha256}\n`
|
|
)
|
|
NODE
|
|
|
|
- name: Record verified Agent build
|
|
shell: bash
|
|
env:
|
|
AGENT_ARCH: ${{ matrix.arch }}
|
|
AGENT_PLATFORM: ${{ matrix.platform }}
|
|
AGENT_SHA256: ${{ steps.agent.outputs.sha256 }}
|
|
run: |
|
|
{
|
|
echo "### $AGENT_PLATFORM $AGENT_ARCH Agent build"
|
|
echo
|
|
echo "Native compound Agent, Node, and OpenCode Runtime build, test-signature verification, launch smoke, and deterministic archive comparison passed."
|
|
echo
|
|
echo "Archive SHA-256: \`$AGENT_SHA256\`"
|
|
echo
|
|
echo "The ephemeral CI key and bundle are not published."
|
|
echo "Native build checks do not replace real Host installation, Attach, and Ask/Execute acceptance."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|