feat: ship skill and MCP workflows with signed updater for v0.0.7

This commit is contained in:
xiaohan.yuan committed 2026-10-21 00:04:17 +08:00
1 parent 1b52df0603
commit a01085c656
136 files changed
+18646 -217

No files matched your search

+408
View File
@@ -0,0 +1,408 @@
import crypto from 'node:crypto'
import fsp from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import {
DEFAULT_BASE_URL,
SERVER_RELEASES_ROOT,
SERVER_STAGING_ROOT,
assert,
compareVersions,
normalizeBaseUrl,
parseVersion,
readJson,
readPublicKey,
renderDownloadPage,
serializeSignedManifest,
sha256File,
verifyReleaseInstaller,
verifySignedManifest,
runProcess,
runSsh,
validateSshHost,
writeFileAtomic
} from './lib.mjs'
const MAX_MANIFEST_BYTES = 1024 * 1024
export function parseCurrentManifest(raw, publicKeyPem, baseUrl) {
assert(Buffer.byteLength(raw, 'utf8') <= MAX_MANIFEST_BYTES, 'Current latest.json exceeds 1 MiB')
let document
try {
document = JSON.parse(raw)
} catch {
throw new Error('Current latest.json is not valid JSON')
}
verifySignedManifest(document, publicKeyPem, { baseUrl })
return document
}
export function assertMonotonicTransition(current, target) {
if (!current) return
const compared = compareVersions(target.payload.version, current.payload.version)
assert(compared >= 0, `Refusing to publish ${target.payload.version}; server latest is newer (${current.payload.version})`)
if (compared === 0) {
assert(
JSON.stringify(current) === JSON.stringify(target),
`Refusing to replace ${target.payload.version} with a different manifest`
)
}
}
export async function validateReleaseBundle({
manifestPath,
installerPath,
publicKeyPath,
baseUrl = DEFAULT_BASE_URL
}) {
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
const publicKeyPem = await readPublicKey(publicKeyPath)
let document
try {
document = await readJson(manifestPath)
} catch (error) {
throw new Error(`Unable to read release manifest: ${error.message}`)
}
verifySignedManifest(document, publicKeyPem, { baseUrl: normalizedBaseUrl })
const expected = document.payload.platforms['windows-x64']
const installer = await verifyReleaseInstaller(installerPath, document.payload.version)
assert(installer.size === expected.size, 'Installer size does not match the signed manifest')
assert(installer.sha256 === expected.sha256, 'Installer SHA-256 does not match the signed manifest')
assert(path.posix.basename(new URL(expected.url).pathname) === installer.filename, 'Signed URL filename does not match the installer')
return {
document,
manifestText: serializeSignedManifest(document, { baseUrl: normalizedBaseUrl }),
pageHtml: renderDownloadPage(document.payload, { baseUrl: normalizedBaseUrl }),
installer,
publicKeyPem,
baseUrl: normalizedBaseUrl
}
}
export async function readLocalLatest(root, publicKeyPem, baseUrl) {
const filename = path.join(root, 'releases', 'latest.json')
try {
const raw = await fsp.readFile(filename, 'utf8')
return { raw, document: parseCurrentManifest(raw, publicKeyPem, baseUrl) }
} catch (error) {
if (error && error.code === 'ENOENT') return { raw: null, document: null }
throw error
}
}
async function installVersionDirectory({ root, version, installerPath, expectedSha256, runId }) {
const releases = path.join(root, 'releases')
const versions = path.join(releases, 'versions')
const target = path.join(versions, version)
const filename = path.basename(installerPath)
await fsp.mkdir(versions, { recursive: true })
try {
const entries = await fsp.readdir(target)
assert(entries.length === 1 && entries[0] === filename, `Version directory ${version} already conflicts with this artifact`)
const existingHash = await sha256File(path.join(target, filename))
assert(existingHash === expectedSha256, `Version directory ${version} contains a conflicting artifact`)
return
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error
}
const staging = path.join(root, '.staging', runId)
const stagedVersion = path.join(staging, 'version')
const stagedFile = path.join(stagedVersion, filename)
await fsp.mkdir(stagedVersion, { recursive: true })
try {
await fsp.copyFile(installerPath, stagedFile)
const copiedHash = await sha256File(stagedFile)
assert(copiedHash === expectedSha256, 'Staged installer hash changed')
await fsp.rename(stagedVersion, target)
} finally {
await fsp.rm(staging, { recursive: true, force: true })
}
}
export async function publishLocalRelease({
root,
release,
current,
beforeLatestSwap
}) {
const { document, installer, manifestText, publicKeyPem, baseUrl } = release
assertMonotonicTransition(current?.document, document)
await installVersionDirectory({
root,
version: document.payload.version,
installerPath: installer.path,
expectedSha256: document.payload.platforms['windows-x64'].sha256,
runId: crypto.randomBytes(12).toString('hex')
})
const latestPath = path.join(root, 'releases', 'latest.json')
const indexPath = path.join(root, 'releases', 'index.html')
let previousPage = null
try {
previousPage = await fsp.readFile(indexPath)
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error
}
const assertCurrentLatest = async () => {
if (current?.raw != null) {
const liveRaw = await fsp.readFile(latestPath, 'utf8')
assert(liveRaw === current.raw, 'latest.json changed during publication')
return
}
try {
await fsp.access(latestPath)
throw new Error('latest.json appeared during publication')
} catch (error) {
if (error && error.code !== 'ENOENT') throw error
}
}
await assertCurrentLatest()
try {
await writeFileAtomic(
indexPath,
release.pageHtml || renderDownloadPage(document.payload, { baseUrl }),
0o644
)
if (beforeLatestSwap) await beforeLatestSwap()
await assertCurrentLatest()
await writeFileAtomic(latestPath, manifestText, 0o644)
} catch (error) {
try {
if (previousPage == null) {
await fsp.rm(indexPath, { force: true })
} else {
await writeFileAtomic(indexPath, previousPage, 0o644)
}
} catch (rollbackError) {
throw new Error(`${error.message}; failed to restore download page: ${rollbackError.message}`)
}
throw error
}
return { latestPath, document }
}
export async function readLatestViaSsh(host, publicKeyPem, baseUrl) {
const script = `set -euo pipefail
if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then
cat '${SERVER_RELEASES_ROOT}/latest.json'
fi
`
const raw = runSsh(host, script)
if (!raw) return { raw: null, document: null }
assert(Buffer.byteLength(raw, 'utf8') <= MAX_MANIFEST_BYTES, 'Server latest.json exceeds 1 MiB')
return { raw, document: parseCurrentManifest(raw, publicKeyPem, baseUrl) }
}
function sha256Text(value) {
return crypto.createHash('sha256').update(value, 'utf8').digest('hex')
}
export async function publishViaSsh({
host,
release,
current
}) {
validateSshHost(host)
const { document, installer, manifestText, publicKeyPem, baseUrl } = release
assertMonotonicTransition(current?.document, document)
const runId = crypto.randomBytes(12).toString('hex')
const version = document.payload.version
const filename = installer.filename
parseVersion(version)
assert(/^[A-Za-z0-9._-]+\.exe$/.test(filename), 'Unsafe installer filename')
const staging = `${SERVER_STAGING_ROOT}/${runId}`
const manifestSha256 = sha256Text(manifestText)
const pageHtml = renderDownloadPage(document.payload, { baseUrl })
const pageSha256 = sha256Text(pageHtml)
const metadata = {
runId,
version,
filename,
sha256: document.payload.platforms['windows-x64'].sha256,
size: document.payload.platforms['windows-x64'].size,
manifestSha256,
pageSha256,
expectedLatestSha256: current?.raw == null ? null : sha256Text(current.raw)
}
runSsh(host, `set -euo pipefail
install -d -o root -g root -m 0700 '${SERVER_STAGING_ROOT}'
install -d -o root -g root -m 0700 '${staging}'
`)
let publicationError
try {
runProcess('scp', ['-q', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', installer.path, `${host}:${staging}/${filename}`])
const temporary = await fsp.mkdtemp(path.join(os.tmpdir(), 'ycode-update-deploy-'))
const localManifest = path.join(temporary, 'latest.json')
const localPage = path.join(temporary, 'index.html')
await fsp.writeFile(localManifest, manifestText, { mode: 0o600 })
await fsp.writeFile(localPage, pageHtml, { mode: 0o600 })
try {
runProcess('scp', ['-q', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', localManifest, `${host}:${staging}/latest.json`])
runProcess('scp', ['-q', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', localPage, `${host}:${staging}/index.html`])
} finally {
await fsp.rm(temporary, { recursive: true, force: true })
}
const metadataBase64 = Buffer.from(JSON.stringify(metadata), 'utf8').toString('base64')
const remoteScript = `set -euo pipefail
umask 022
metadata_file="$(mktemp /tmp/ycode-release.XXXXXX.json)"
cleanup() {
rm -f "$metadata_file"
rm -rf '${staging}'
}
trap cleanup EXIT
printf '%s' '${metadataBase64}' | base64 -d > "$metadata_file"
python3 - "$metadata_file" <<'PY'
import fcntl
import hashlib
import json
import os
import re
import shutil
import sys
meta = json.load(open(sys.argv[1], "r", encoding="utf-8"))
run_id = meta["runId"]
version = meta["version"]
filename = meta["filename"]
expected_hash = meta["sha256"]
expected_size = meta["size"]
manifest_hash = meta["manifestSha256"]
page_hash = meta["pageSha256"]
expected_latest_hash = meta["expectedLatestSha256"]
if not re.fullmatch(r"[0-9A-Fa-f]{24}", run_id):
raise SystemExit("invalid staging id")
if not re.fullmatch(r"[0-9A-Za-z._-]+\\.exe", filename):
raise SystemExit("invalid installer filename")
if not re.fullmatch(r"[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?", version):
raise SystemExit("invalid version")
public_root = "${path.posix.dirname(SERVER_RELEASES_ROOT)}"
releases = "${SERVER_RELEASES_ROOT}"
staging = "${SERVER_STAGING_ROOT}/" + run_id
versions = os.path.join(releases, "versions")
target_dir = os.path.join(versions, version)
staged_artifact = os.path.join(staging, filename)
staged_manifest = os.path.join(staging, "latest.json")
staged_page = os.path.join(staging, "index.html")
latest = os.path.join(releases, "latest.json")
index = os.path.join(releases, "index.html")
def digest(pathname):
value = hashlib.sha256()
with open(pathname, "rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
value.update(chunk)
return value.hexdigest()
if os.stat(staging).st_dev != os.stat(public_root).st_dev:
raise SystemExit("staging and release roots are on different filesystems")
os.makedirs(versions, mode=0o755, exist_ok=True)
os.makedirs("${SERVER_STAGING_ROOT}", mode=0o700, exist_ok=True)
lock_path = "/var/lock/ycode-release.lock"
with open(lock_path, "a+", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
live_latest_hash = digest(latest) if os.path.isfile(latest) else None
if live_latest_hash != expected_latest_hash:
raise SystemExit("latest.json changed during publication")
if os.path.getsize(staged_artifact) != expected_size or digest(staged_artifact) != expected_hash:
raise SystemExit("staged installer does not match signed metadata")
if os.path.getsize(staged_manifest) > 1024 * 1024 or digest(staged_manifest) != manifest_hash:
raise SystemExit("staged manifest does not match expected bytes")
if os.path.getsize(staged_page) > 512 * 1024 or digest(staged_page) != page_hash:
raise SystemExit("staged download page does not match expected bytes")
if os.path.exists(target_dir):
entries = sorted(os.listdir(target_dir))
if entries != [filename]:
raise SystemExit("version directory conflicts with this artifact")
if digest(os.path.join(target_dir, filename)) != expected_hash:
raise SystemExit("version directory contains a conflicting artifact")
else:
version_stage = os.path.join(staging, "version")
os.makedirs(version_stage, mode=0o755, exist_ok=False)
os.replace(staged_artifact, os.path.join(version_stage, filename))
os.chmod(os.path.join(version_stage, filename), 0o644)
os.replace(version_stage, target_dir)
previous_page = None
if os.path.isfile(index):
with open(index, "rb") as stream:
previous_page = stream.read()
if len(previous_page) > 512 * 1024:
raise SystemExit("existing download page is unexpectedly large")
next_latest = os.path.join(releases, f".latest.json.{run_id}.tmp")
next_page = os.path.join(releases, f".index.html.{run_id}.tmp")
os.replace(staged_manifest, next_latest)
os.chmod(next_latest, 0o644)
with open(next_latest, "rb") as stream:
os.fsync(stream.fileno())
os.replace(staged_page, next_page)
os.chmod(next_page, 0o644)
with open(next_page, "rb") as stream:
os.fsync(stream.fileno())
page_swapped = False
try:
os.replace(next_page, index)
page_swapped = True
os.replace(next_latest, latest)
except Exception:
if page_swapped:
if previous_page is None:
try:
os.unlink(index)
except FileNotFoundError:
pass
else:
rollback_page = os.path.join(releases, f".index.html.{run_id}.rollback")
with open(rollback_page, "wb") as stream:
stream.write(previous_page)
stream.flush()
os.fsync(stream.fileno())
os.chmod(rollback_page, 0o644)
os.replace(rollback_page, index)
raise
directory_fd = os.open(releases, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
print(version)
PY
`
runSsh(host, remoteScript)
} catch (error) {
publicationError = error
throw error
} finally {
try {
runSsh(host, `set -euo pipefail
rm -rf '${staging}'
`)
} catch (cleanupError) {
if (publicationError) {
console.error(`Unable to remove remote staging path after failure: ${cleanupError.message}`)
} else {
console.warn(`Publication succeeded, but remote staging cleanup failed: ${cleanupError.message}`)
}
}
}
return { version }
}