Check cached embedded servers before reuse, isolate concurrent cancellation, and serialize request MCP registration and cleanup. Preserve native controls across session switches and recover usable partial inventories. Share the bundled offline OpenCode configuration with desktop and Agent launch paths, including model bridge environment forwarding. Validation: focused tests and typecheck passed; lint passed before the final fixes. The full test run exceeded 120 seconds and reported a DS stdio MCP failure. Current-source Agent host and real suspend/resume validation remain pending. Release note: 修复切换会话后 OpenCode 原生控件消失及并发取消相互影响的问题,并完善内嵌服务复用检查和桌面、Agent 的离线依赖配置。
1064 lines
32 KiB
JavaScript
1064 lines
32 KiB
JavaScript
const { createHash, createPublicKey, sign, verify } = require("node:crypto");
|
|
const {
|
|
chmodSync,
|
|
closeSync,
|
|
cpSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
openSync,
|
|
readFileSync,
|
|
readSync,
|
|
readdirSync,
|
|
renameSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} = require("node:fs");
|
|
const { tmpdir } = require("node:os");
|
|
const {
|
|
basename,
|
|
dirname,
|
|
join,
|
|
posix,
|
|
relative,
|
|
resolve,
|
|
sep,
|
|
} = require("node:path");
|
|
const tar = require("tar");
|
|
const { targetName, assertTargetBinary } = require("./agent-build-target.cjs");
|
|
const {
|
|
preflightRegisteredProductionKey,
|
|
} = require("./signing-key-preflight.cjs");
|
|
const { prepareBundledOpenCodeConfig } = require("./opencode-config.cjs");
|
|
|
|
const root = join(__dirname, "..");
|
|
const supportedArchitectures = Object.freeze(["x64", "arm64"]);
|
|
const manifestFileName = "manifest.json";
|
|
const signatureFileName = "manifest.sig";
|
|
const signingPrivateKeyEnvironment = "GOODBUDDY_SIGNING_PRIVATE_KEY";
|
|
const signingKeyIdEnvironment = "GOODBUDDY_SIGNING_KEY_ID";
|
|
const signatureDomain = Buffer.from(
|
|
"GoodBuddy Remote Runtime Bundle Manifest Signature v1\0",
|
|
"utf8",
|
|
);
|
|
const profileDigests = Object.freeze({
|
|
adapter:
|
|
"sha256:ac7696abe504bbea444d8ad80dd8faa96437e6dbb27ed987ce46e1045f3dd365",
|
|
acp: "sha256:0e3764ab897258bc0234162357c2eefc3faeb9918080ae81b60e2084ed7996a6",
|
|
});
|
|
const fixedLimits = Object.freeze({
|
|
maximumPromptRuntimeMilliseconds: 10 * 60 * 1000,
|
|
maximumPromptInputBytes: 16 * 1024 * 1024,
|
|
maximumPromptOutputBytes: 8 * 1024 * 1024,
|
|
});
|
|
|
|
function readJson(filePath, description) {
|
|
try {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
} catch (error) {
|
|
throw new Error(
|
|
`${description} is missing or invalid: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
}
|
|
|
|
function readRemoteRuntimeLock(projectRoot = root) {
|
|
const lock = readJson(
|
|
join(projectRoot, "remote-runtime-lock.json"),
|
|
"Remote Runtime lock",
|
|
);
|
|
const runtime = lock?.runtimes?.opencode;
|
|
if (
|
|
lock?.formatVersion !== 1 ||
|
|
runtime?.version !== "1.18.29" ||
|
|
runtime.provider !== "opencode" ||
|
|
runtime.entrypoint !== "bin/opencode" ||
|
|
runtime.entrypointIdentity !== "opencode-acp" ||
|
|
JSON.stringify(runtime.argvPrefix) !== JSON.stringify(["acp"]) ||
|
|
!Number.isSafeInteger(runtime.protocol?.major) ||
|
|
!Number.isSafeInteger(runtime.protocol?.minor)
|
|
) {
|
|
throw new Error("Remote Runtime lock contract is invalid");
|
|
}
|
|
for (const architecture of supportedArchitectures) {
|
|
const target = runtime.targets?.[architecture];
|
|
if (
|
|
typeof target?.package !== "string" ||
|
|
typeof target.integrity !== "string" ||
|
|
!/^sha512-[A-Za-z0-9+/]{86}==$/u.test(target.integrity)
|
|
) {
|
|
throw new Error(`Remote Runtime lock target is invalid: ${architecture}`);
|
|
}
|
|
}
|
|
return lock;
|
|
}
|
|
|
|
function readTrustedKeyRegistry(projectRoot = root) {
|
|
const registry = readJson(
|
|
join(projectRoot, "resources", "agent-release-keys.json"),
|
|
"Runtime trusted key registry",
|
|
);
|
|
if (
|
|
registry?.formatVersion !== 1 ||
|
|
!Array.isArray(registry.keys) ||
|
|
!Array.isArray(registry.revocations)
|
|
) {
|
|
throw new Error("Runtime trusted key registry contract is invalid");
|
|
}
|
|
const keyIds = new Set();
|
|
for (const key of registry.keys) {
|
|
if (
|
|
typeof key?.keyId !== "string" ||
|
|
!/^[a-z0-9][a-z0-9._-]{0,63}$/u.test(key.keyId) ||
|
|
keyIds.has(key.keyId) ||
|
|
typeof key.publicKeySpkiBase64 !== "string" ||
|
|
!["production", "test"].includes(key.environment)
|
|
) {
|
|
throw new Error("Runtime trusted key registry entry is invalid");
|
|
}
|
|
const publicKey = createPublicKey({
|
|
key: Buffer.from(key.publicKeySpkiBase64, "base64"),
|
|
format: "der",
|
|
type: "spki",
|
|
});
|
|
if (publicKey.asymmetricKeyType !== "ed25519") {
|
|
throw new Error(`Runtime trusted key is not Ed25519: ${key.keyId}`);
|
|
}
|
|
keyIds.add(key.keyId);
|
|
}
|
|
return registry;
|
|
}
|
|
|
|
function sha256(contents) {
|
|
return createHash("sha256").update(contents).digest("hex");
|
|
}
|
|
|
|
function sha512FileIntegrity(filePath) {
|
|
const hash = createHash("sha512");
|
|
const descriptor = openSync(filePath, "r");
|
|
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
try {
|
|
while (true) {
|
|
const bytesRead = readSync(descriptor, chunk, 0, chunk.byteLength, null);
|
|
if (bytesRead === 0) {
|
|
break;
|
|
}
|
|
hash.update(chunk.subarray(0, bytesRead));
|
|
}
|
|
} finally {
|
|
closeSync(descriptor);
|
|
}
|
|
return `sha512-${hash.digest("base64")}`;
|
|
}
|
|
|
|
function sha256FileSync(filePath) {
|
|
return sha256(readFileSync(filePath));
|
|
}
|
|
|
|
function canonicalJson(value) {
|
|
if (
|
|
value === null ||
|
|
typeof value === "boolean" ||
|
|
typeof value === "string"
|
|
) {
|
|
return JSON.stringify(value);
|
|
}
|
|
if (typeof value === "number") {
|
|
if (!Number.isFinite(value)) {
|
|
throw new Error("Canonical Runtime value is not finite");
|
|
}
|
|
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return `[${value.map(canonicalJson).join(",")}]`;
|
|
}
|
|
if (
|
|
typeof value !== "object" ||
|
|
Object.getPrototypeOf(value) !== Object.prototype
|
|
) {
|
|
throw new Error("Canonical Runtime value is not plain JSON");
|
|
}
|
|
return `{${Object.keys(value)
|
|
.sort()
|
|
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
.join(",")}}`;
|
|
}
|
|
|
|
function digestCanonicalOperation({ method, scope, payload }) {
|
|
return `sha256:${sha256(
|
|
Buffer.from(canonicalJson({ method, payload, scope }), "utf8"),
|
|
)}`;
|
|
}
|
|
|
|
function digestRuntimeBundleIdentity(manifest) {
|
|
const { bundleDigest: _bundleDigest, ...identity } = manifest;
|
|
void _bundleDigest;
|
|
return digestCanonicalOperation({
|
|
method: "runtime/bundleIdentity",
|
|
scope: {
|
|
kind: "installation",
|
|
installationId: manifest.runtimeId,
|
|
},
|
|
payload: identity,
|
|
});
|
|
}
|
|
|
|
function digestRuntimeBundleManifest(manifest) {
|
|
return digestCanonicalOperation({
|
|
method: "runtime/bundleManifest",
|
|
scope: {
|
|
kind: "installation",
|
|
installationId: manifest.runtimeId,
|
|
},
|
|
payload: manifest,
|
|
});
|
|
}
|
|
|
|
function canonicalManifestBytes(manifest) {
|
|
return Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
function signaturePayload(manifestBytes) {
|
|
return Buffer.concat([signatureDomain, manifestBytes]);
|
|
}
|
|
|
|
function publicKeySpkiBase64(key) {
|
|
return createPublicKey(key)
|
|
.export({ format: "der", type: "spki" })
|
|
.toString("base64");
|
|
}
|
|
|
|
function productionSigningIdentity(environment = process.env) {
|
|
const keyId = environment[signingKeyIdEnvironment];
|
|
const privateKey = environment[signingPrivateKeyEnvironment];
|
|
if (!keyId || !privateKey) {
|
|
throw new Error(
|
|
`${signingKeyIdEnvironment} and ${signingPrivateKeyEnvironment} are required to sign the bundled Runtime`,
|
|
);
|
|
}
|
|
return { keyId, privateKey };
|
|
}
|
|
|
|
function trustedKeyForSigning(
|
|
signingIdentity,
|
|
registry,
|
|
verificationEnvironment,
|
|
) {
|
|
const record = registry.keys.find(
|
|
(key) => key.keyId === signingIdentity.keyId,
|
|
);
|
|
if (!record) {
|
|
throw new Error(
|
|
`Runtime signing key is not registered: ${signingIdentity.keyId}`,
|
|
);
|
|
}
|
|
if (
|
|
verificationEnvironment === "production" &&
|
|
record.environment !== "production"
|
|
) {
|
|
throw new Error(
|
|
`Runtime signing key is not registered for production: ${record.keyId}`,
|
|
);
|
|
}
|
|
if (
|
|
registry.revocations.some((revocation) => revocation.keyId === record.keyId)
|
|
) {
|
|
throw new Error("Runtime signing key is outside its trusted interval");
|
|
}
|
|
if (
|
|
publicKeySpkiBase64(signingIdentity.privateKey) !==
|
|
record.publicKeySpkiBase64
|
|
) {
|
|
throw new Error(
|
|
"Runtime signing private key does not match the trusted registry",
|
|
);
|
|
}
|
|
return record;
|
|
}
|
|
|
|
function preflightProductionSigningKey(options = {}) {
|
|
const projectRoot = options.projectRoot ?? root;
|
|
const environment = options.environment ?? process.env;
|
|
const keyId = options.keyId ?? environment[signingKeyIdEnvironment];
|
|
const registry = options.registry ?? readTrustedKeyRegistry(projectRoot);
|
|
return preflightRegisteredProductionKey({
|
|
component: "GoodBuddy",
|
|
keyId,
|
|
registry,
|
|
missingKeyIdMessage: `${signingKeyIdEnvironment} is not configured; provision the production GoodBuddy signing key ID before building a release`,
|
|
});
|
|
}
|
|
|
|
function assertSafeManifestPath(filePath) {
|
|
if (
|
|
typeof filePath !== "string" ||
|
|
filePath.length === 0 ||
|
|
filePath.length > 240 ||
|
|
filePath.includes("\\") ||
|
|
filePath.includes("\0") ||
|
|
posix.isAbsolute(filePath) ||
|
|
filePath
|
|
.split("/")
|
|
.some((part) => part.length === 0 || part === "." || part === "..")
|
|
) {
|
|
throw new Error(`Unsafe Runtime manifest path: ${String(filePath)}`);
|
|
}
|
|
}
|
|
|
|
function compareUtf8(left, right) {
|
|
return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
|
|
}
|
|
|
|
function listFiles(directory) {
|
|
const files = [];
|
|
const pending = [directory];
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
const entries = readdirSync(current, {
|
|
withFileTypes: true,
|
|
}).sort((left, right) => compareUtf8(right.name, left.name));
|
|
for (const entry of entries) {
|
|
const absolutePath = join(current, entry.name);
|
|
if (entry.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Runtime bundle cannot contain a symlink: ${absolutePath}`,
|
|
);
|
|
}
|
|
if (entry.isDirectory()) {
|
|
pending.push(absolutePath);
|
|
} else if (entry.isFile()) {
|
|
files.push(absolutePath);
|
|
} else {
|
|
throw new Error(
|
|
`Runtime bundle contains an unsupported file: ${absolutePath}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return files.sort(compareUtf8);
|
|
}
|
|
|
|
function posixRelative(directory, filePath) {
|
|
return relative(directory, filePath).split(sep).join("/");
|
|
}
|
|
|
|
function modeString(filePath) {
|
|
return (statSync(filePath).mode & 0o777).toString(8).padStart(4, "0");
|
|
}
|
|
|
|
function assertElfArchitecture(
|
|
filePath,
|
|
expectedArchitecture,
|
|
platform = "linux",
|
|
) {
|
|
assertTargetBinary(filePath, expectedArchitecture, platform, "Runtime");
|
|
}
|
|
|
|
function validateManifestShape(manifest) {
|
|
if (
|
|
manifest?.formatVersion !== 2 ||
|
|
manifest.product !== "GoodBuddy" ||
|
|
manifest.runtimeId !== "opencode" ||
|
|
manifest.runtimeVersion !== "1.18.29" ||
|
|
manifest.provider !== "opencode" ||
|
|
!(
|
|
manifest.platform === "linux" ||
|
|
(manifest.platform === "darwin" && manifest.architecture === "arm64")
|
|
) ||
|
|
!supportedArchitectures.includes(manifest.architecture) ||
|
|
typeof manifest.signingKeyId !== "string" ||
|
|
!/^sha256:[a-f0-9]{64}$/u.test(manifest.bundleDigest) ||
|
|
!/^sha256:[a-f0-9]{64}$/u.test(manifest.adapterDigest) ||
|
|
!/^sha256:[a-f0-9]{64}$/u.test(manifest.acpCapabilitiesDigest) ||
|
|
!Array.isArray(manifest.files) ||
|
|
!Array.isArray(manifest.licenses) ||
|
|
!Array.isArray(manifest.allowedEnvironmentNames)
|
|
) {
|
|
throw new Error("Runtime manifest contract is invalid");
|
|
}
|
|
if (
|
|
!Number.isSafeInteger(manifest.limits?.maximumPromptRuntimeMilliseconds) ||
|
|
manifest.limits.maximumPromptRuntimeMilliseconds < 1 ||
|
|
!Number.isSafeInteger(manifest.limits.maximumPromptInputBytes) ||
|
|
manifest.limits.maximumPromptInputBytes < 1 ||
|
|
!Number.isSafeInteger(manifest.limits.maximumPromptOutputBytes) ||
|
|
manifest.limits.maximumPromptOutputBytes < 0 ||
|
|
Object.keys(manifest.limits).length !== 3 ||
|
|
"quotas" in manifest
|
|
) {
|
|
throw new Error("Runtime manifest limits contract is invalid");
|
|
}
|
|
if (
|
|
manifest.entrypoint?.identity !== "opencode-acp" ||
|
|
manifest.entrypoint.path !== "bin/opencode" ||
|
|
!/^[a-f0-9]{64}$/u.test(manifest.entrypoint.sha256) ||
|
|
JSON.stringify(manifest.entrypoint.argvPrefix) !== JSON.stringify(["acp"])
|
|
) {
|
|
throw new Error("Runtime manifest entrypoint is invalid");
|
|
}
|
|
}
|
|
|
|
function trustedKeyForManifest(manifest, registry, verificationEnvironment) {
|
|
const key = registry.keys.find(
|
|
(candidate) => candidate.keyId === manifest.signingKeyId,
|
|
);
|
|
if (!key) {
|
|
throw new Error(
|
|
`Runtime manifest uses an unknown signing key: ${manifest.signingKeyId}`,
|
|
);
|
|
}
|
|
if (
|
|
verificationEnvironment === "production" &&
|
|
key.environment !== "production"
|
|
) {
|
|
throw new Error(
|
|
`Production Runtime verification rejects non-production key: ${key.keyId}`,
|
|
);
|
|
}
|
|
if (
|
|
registry.revocations.some((revocation) => revocation.keyId === key.keyId)
|
|
) {
|
|
throw new Error("Runtime signing key is outside its trusted interval");
|
|
}
|
|
return key;
|
|
}
|
|
|
|
function assertManifestMatchesLock(
|
|
manifest,
|
|
lock,
|
|
architecture,
|
|
platform = "linux",
|
|
) {
|
|
const expected = lock.runtimes.opencode;
|
|
const target =
|
|
expected.targets[
|
|
platform === "linux" ? architecture : targetName(architecture, platform)
|
|
];
|
|
if (
|
|
manifest.runtimeId !== "opencode" ||
|
|
manifest.runtimeVersion !== expected.version ||
|
|
manifest.provider !== expected.provider ||
|
|
manifest.platform !== platform ||
|
|
manifest.architecture !== architecture ||
|
|
manifest.sourcePackage?.name !== target.package ||
|
|
manifest.sourcePackage.integrity !== target.integrity ||
|
|
manifest.entrypoint.path !== expected.entrypoint ||
|
|
manifest.entrypoint.identity !== expected.entrypointIdentity ||
|
|
JSON.stringify(manifest.entrypoint.argvPrefix) !==
|
|
JSON.stringify(expected.argvPrefix) ||
|
|
JSON.stringify(manifest.allowedEnvironmentNames) !==
|
|
JSON.stringify(expected.allowedEnvironmentNames) ||
|
|
manifest.protocol?.major !== expected.protocol.major ||
|
|
manifest.protocol?.minor !== expected.protocol.minor ||
|
|
manifest.adapterDigest !== profileDigests.adapter ||
|
|
manifest.acpCapabilitiesDigest !== profileDigests.acp
|
|
) {
|
|
throw new Error(
|
|
"Runtime manifest does not match the locked OpenCode profile",
|
|
);
|
|
}
|
|
}
|
|
|
|
function verifyBundleDirectory(bundleDirectoryInput, options = {}) {
|
|
const bundleDirectory = resolve(bundleDirectoryInput);
|
|
const projectRoot = options.projectRoot ?? root;
|
|
const lock = options.lock ?? readRemoteRuntimeLock(projectRoot);
|
|
const registry = options.registry ?? readTrustedKeyRegistry(projectRoot);
|
|
const architecture = options.architecture;
|
|
const platform = options.platform ?? "linux";
|
|
targetName(architecture, platform);
|
|
const manifestBytes = readFileSync(join(bundleDirectory, manifestFileName));
|
|
const signatureText = readFileSync(
|
|
join(bundleDirectory, signatureFileName),
|
|
"utf8",
|
|
);
|
|
if (!/^[A-Za-z0-9+/]{86}==\n$/u.test(signatureText)) {
|
|
throw new Error("Runtime detached signature encoding is invalid");
|
|
}
|
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
validateManifestShape(manifest);
|
|
if (!canonicalManifestBytes(manifest).equals(manifestBytes)) {
|
|
throw new Error("Runtime manifest is not in canonical deterministic form");
|
|
}
|
|
assertManifestMatchesLock(manifest, lock, architecture, platform);
|
|
const environment = options.verificationEnvironment ?? "production";
|
|
const key = trustedKeyForManifest(manifest, registry, environment);
|
|
const publicKey = createPublicKey({
|
|
key: Buffer.from(key.publicKeySpkiBase64, "base64"),
|
|
format: "der",
|
|
type: "spki",
|
|
});
|
|
const signatureBytes = Buffer.from(signatureText.trim(), "base64");
|
|
if (
|
|
signatureBytes.length !== 64 ||
|
|
!verify(null, signaturePayload(manifestBytes), publicKey, signatureBytes)
|
|
) {
|
|
throw new Error("Runtime manifest signature verification failed");
|
|
}
|
|
if (digestRuntimeBundleIdentity(manifest) !== manifest.bundleDigest) {
|
|
throw new Error("Runtime bundle identity digest does not match");
|
|
}
|
|
if (
|
|
options.requireDigestDirectory !== false &&
|
|
basename(bundleDirectory) !== manifest.bundleDigest.slice("sha256:".length)
|
|
) {
|
|
throw new Error(
|
|
"Runtime bundle digest does not match its managed directory",
|
|
);
|
|
}
|
|
|
|
const declared = new Set();
|
|
for (const file of manifest.files) {
|
|
assertSafeManifestPath(file.path);
|
|
if (
|
|
declared.has(file.path) ||
|
|
!Number.isSafeInteger(file.size) ||
|
|
file.size < 0 ||
|
|
!/^[a-f0-9]{64}$/u.test(file.sha256) ||
|
|
!["0644", "0755"].includes(file.mode)
|
|
) {
|
|
throw new Error(`Runtime manifest file entry is invalid: ${file.path}`);
|
|
}
|
|
declared.add(file.path);
|
|
const filePath = join(bundleDirectory, ...file.path.split("/"));
|
|
const stat = lstatSync(filePath);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(`Runtime payload is not a regular file: ${file.path}`);
|
|
}
|
|
if (stat.size !== file.size) {
|
|
throw new Error(`Runtime payload size mismatch: ${file.path}`);
|
|
}
|
|
if (sha256FileSync(filePath) !== file.sha256) {
|
|
throw new Error(`Runtime payload hash mismatch: ${file.path}`);
|
|
}
|
|
if (
|
|
options.enforceFilesystemMode !== false &&
|
|
process.platform !== "win32" &&
|
|
modeString(filePath) !== file.mode
|
|
) {
|
|
throw new Error(`Runtime payload mode mismatch: ${file.path}`);
|
|
}
|
|
}
|
|
const actual = new Set(
|
|
listFiles(bundleDirectory).map((filePath) =>
|
|
posixRelative(bundleDirectory, filePath),
|
|
),
|
|
);
|
|
const expected = new Set([...declared, manifestFileName, signatureFileName]);
|
|
if (
|
|
actual.size !== expected.size ||
|
|
[...actual].some((filePath) => !expected.has(filePath))
|
|
) {
|
|
throw new Error("Runtime bundle contains undeclared or missing files");
|
|
}
|
|
for (const license of manifest.licenses) {
|
|
assertSafeManifestPath(license.path);
|
|
if (!declared.has(license.path)) {
|
|
throw new Error("Runtime manifest license is not declared");
|
|
}
|
|
}
|
|
if (
|
|
!declared.has(manifest.entrypoint.path) ||
|
|
manifest.entrypoint.sha256 !==
|
|
manifest.files.find((file) => file.path === manifest.entrypoint.path)
|
|
?.sha256
|
|
) {
|
|
throw new Error("Runtime entrypoint payload is missing");
|
|
}
|
|
assertElfArchitecture(
|
|
join(bundleDirectory, ...manifest.entrypoint.path.split("/")),
|
|
architecture,
|
|
platform,
|
|
);
|
|
return {
|
|
bundleDirectory,
|
|
manifest,
|
|
manifestDigest: digestRuntimeBundleManifest(manifest),
|
|
};
|
|
}
|
|
|
|
function lockedArchive(lock, architecture, archivePath, platform = "linux") {
|
|
const input = resolveLockedRuntimeInput(lock, architecture, platform);
|
|
if (!archivePath || !existsSync(archivePath)) {
|
|
throw new Error(`Locked OpenCode archive is required: ${input.archive}`);
|
|
}
|
|
if (!statSync(archivePath).isFile()) {
|
|
throw new Error(`Runtime archive is not a file: ${archivePath}`);
|
|
}
|
|
if (basename(archivePath) !== input.archive) {
|
|
throw new Error(
|
|
`Runtime archive filename mismatch: expected ${input.archive}`,
|
|
);
|
|
}
|
|
const actualIntegrity = sha512FileIntegrity(archivePath);
|
|
if (actualIntegrity !== input.integrity) {
|
|
throw new Error(
|
|
`Runtime archive integrity mismatch for ${targetName(architecture)}`,
|
|
);
|
|
}
|
|
return lock.runtimes.opencode.targets[
|
|
platform === "linux" ? architecture : targetName(architecture, platform)
|
|
];
|
|
}
|
|
|
|
function resolveLockedRuntimeInput(lock, architecture, platform = "linux") {
|
|
targetName(architecture, platform);
|
|
const runtime = lock.runtimes.opencode;
|
|
const target =
|
|
runtime.targets[
|
|
platform === "linux" ? architecture : targetName(architecture, platform)
|
|
];
|
|
return {
|
|
packageName: target.package,
|
|
version: runtime.version,
|
|
integrity: target.integrity,
|
|
archive: `${target.package}-${runtime.version}.tgz`,
|
|
};
|
|
}
|
|
|
|
function createManifest(bundleDirectory, metadata) {
|
|
const files = listFiles(bundleDirectory)
|
|
.filter((filePath) => {
|
|
const name = posixRelative(bundleDirectory, filePath);
|
|
return name !== manifestFileName && name !== signatureFileName;
|
|
})
|
|
.map((filePath) => {
|
|
const path = posixRelative(bundleDirectory, filePath);
|
|
assertSafeManifestPath(path);
|
|
return {
|
|
path,
|
|
size: statSync(filePath).size,
|
|
sha256: sha256FileSync(filePath),
|
|
mode: path === "bin/opencode" ? "0755" : "0644",
|
|
};
|
|
});
|
|
const entrypoint = files.find((file) => file.path === "bin/opencode");
|
|
if (!entrypoint) {
|
|
throw new Error("Runtime entrypoint is missing");
|
|
}
|
|
const initial = {
|
|
formatVersion: 2,
|
|
product: "GoodBuddy",
|
|
runtimeId: "opencode",
|
|
runtimeVersion: metadata.runtime.version,
|
|
provider: metadata.runtime.provider,
|
|
platform: metadata.platform ?? "linux",
|
|
architecture: metadata.architecture,
|
|
signingKeyId: metadata.signingKeyId,
|
|
bundleDigest: `sha256:${"0".repeat(64)}`,
|
|
adapterDigest: profileDigests.adapter,
|
|
sourcePackage: metadata.sourcePackage,
|
|
entrypoint: {
|
|
identity: metadata.runtime.entrypointIdentity,
|
|
path: metadata.runtime.entrypoint,
|
|
sha256: entrypoint.sha256,
|
|
argvPrefix: metadata.runtime.argvPrefix,
|
|
},
|
|
files,
|
|
licenses: [
|
|
{
|
|
package: "opencode-ai",
|
|
version: metadata.runtime.version,
|
|
spdx: "MIT",
|
|
path: "licenses/opencode-MIT.txt",
|
|
},
|
|
],
|
|
allowedEnvironmentNames: metadata.runtime.allowedEnvironmentNames,
|
|
protocol: metadata.runtime.protocol,
|
|
acpCapabilitiesDigest: profileDigests.acp,
|
|
limits: fixedLimits,
|
|
};
|
|
return {
|
|
...initial,
|
|
bundleDigest: digestRuntimeBundleIdentity(initial),
|
|
};
|
|
}
|
|
|
|
function defaultOutputRoot(projectRoot = root) {
|
|
return join(projectRoot, ".remote-runtime-resources");
|
|
}
|
|
|
|
function bundleDirectory(outputRoot, architecture, digest, platform = "linux") {
|
|
return join(
|
|
outputRoot,
|
|
targetName(architecture, platform),
|
|
"opencode",
|
|
digest.slice("sha256:".length),
|
|
);
|
|
}
|
|
|
|
function buildRuntimeBundle(options) {
|
|
const projectRoot = options.projectRoot ?? root;
|
|
const architecture = options.architecture;
|
|
const platform = options.platform ?? "linux";
|
|
const lock = options.lock ?? readRemoteRuntimeLock(projectRoot);
|
|
const runtime = lock.runtimes.opencode;
|
|
const sourcePackage = lockedArchive(
|
|
lock,
|
|
architecture,
|
|
options.runtimeArchive,
|
|
platform,
|
|
);
|
|
const registry = options.registry ?? readTrustedKeyRegistry(projectRoot);
|
|
const signingIdentity =
|
|
options.testSigningIdentity ??
|
|
options.signingIdentity ??
|
|
productionSigningIdentity(options.environment ?? process.env);
|
|
const verificationEnvironment = options.testSigningIdentity
|
|
? "test"
|
|
: "production";
|
|
trustedKeyForSigning(signingIdentity, registry, verificationEnvironment);
|
|
const outputRoot = options.outputRoot ?? defaultOutputRoot(projectRoot);
|
|
mkdirSync(outputRoot, { recursive: true });
|
|
const staging = mkdtempSync(
|
|
join(outputRoot, `.stage-${targetName(architecture)}-`),
|
|
);
|
|
const extracted = mkdtempSync(
|
|
join(tmpdir(), `goodbuddy-opencode-${targetName(architecture)}-`),
|
|
);
|
|
try {
|
|
tar.x({
|
|
file: options.runtimeArchive,
|
|
cwd: extracted,
|
|
sync: true,
|
|
strict: true,
|
|
filter(path) {
|
|
return (
|
|
path === "package/package.json" || path === "package/bin/opencode"
|
|
);
|
|
},
|
|
});
|
|
const sourceMetadata = readJson(
|
|
join(extracted, "package", "package.json"),
|
|
"OpenCode source package metadata",
|
|
);
|
|
if (
|
|
sourceMetadata.name !== sourcePackage.package &&
|
|
sourceMetadata.name !== sourcePackage.name
|
|
) {
|
|
// The lock target itself carries the canonical package name.
|
|
if (sourceMetadata.name !== sourcePackage.package) {
|
|
throw new Error("OpenCode source package name does not match the lock");
|
|
}
|
|
}
|
|
if (sourceMetadata.version !== runtime.version) {
|
|
throw new Error(
|
|
"OpenCode source package version does not match the lock",
|
|
);
|
|
}
|
|
mkdirSync(join(staging, "bin"), { mode: 0o700 });
|
|
copyFileSync(
|
|
join(extracted, "package", "bin", "opencode"),
|
|
join(staging, "bin", "opencode"),
|
|
);
|
|
chmodSync(join(staging, "bin", "opencode"), 0o755);
|
|
assertElfArchitecture(
|
|
join(staging, "bin", "opencode"),
|
|
architecture,
|
|
platform,
|
|
);
|
|
const configTemplate = prepareBundledOpenCodeConfig(projectRoot);
|
|
cpSync(configTemplate, join(staging, "config", "opencode"), {
|
|
recursive: true,
|
|
});
|
|
mkdirSync(join(staging, "licenses"), { mode: 0o700 });
|
|
copyFileSync(
|
|
join(projectRoot, "node_modules", "opencode-ai", "LICENSE"),
|
|
join(staging, "licenses", "opencode-MIT.txt"),
|
|
);
|
|
chmodSync(join(staging, "licenses", "opencode-MIT.txt"), 0o644);
|
|
const manifest = createManifest(staging, {
|
|
platform,
|
|
architecture,
|
|
runtime,
|
|
sourcePackage: {
|
|
name: sourcePackage.package,
|
|
integrity: sourcePackage.integrity,
|
|
},
|
|
signingKeyId: signingIdentity.keyId,
|
|
});
|
|
const manifestBytes = canonicalManifestBytes(manifest);
|
|
const signatureBytes = sign(
|
|
null,
|
|
signaturePayload(manifestBytes),
|
|
signingIdentity.privateKey,
|
|
);
|
|
writeFileSync(join(staging, manifestFileName), manifestBytes, {
|
|
mode: 0o644,
|
|
});
|
|
writeFileSync(
|
|
join(staging, signatureFileName),
|
|
`${signatureBytes.toString("base64")}\n`,
|
|
{ mode: 0o644 },
|
|
);
|
|
chmodSync(join(staging, manifestFileName), 0o644);
|
|
chmodSync(join(staging, signatureFileName), 0o644);
|
|
const destination = bundleDirectory(
|
|
outputRoot,
|
|
architecture,
|
|
manifest.bundleDigest,
|
|
platform,
|
|
);
|
|
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
if (existsSync(destination)) {
|
|
const existing = verifyBundleDirectory(destination, {
|
|
platform,
|
|
projectRoot,
|
|
architecture,
|
|
lock,
|
|
registry,
|
|
verificationEnvironment,
|
|
enforceFilesystemMode: options.enforceFilesystemMode,
|
|
});
|
|
if (canonicalJson(existing.manifest) !== canonicalJson(manifest)) {
|
|
throw new Error(
|
|
"Existing Runtime digest directory has different content",
|
|
);
|
|
}
|
|
return existing;
|
|
}
|
|
chmodSync(staging, 0o700);
|
|
renameSync(staging, destination);
|
|
return verifyBundleDirectory(destination, {
|
|
platform,
|
|
projectRoot,
|
|
architecture,
|
|
lock,
|
|
registry,
|
|
verificationEnvironment,
|
|
enforceFilesystemMode: options.enforceFilesystemMode,
|
|
});
|
|
} finally {
|
|
rmSync(staging, { recursive: true, force: true });
|
|
rmSync(extracted, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function createRuntimeArchive(bundleDirectoryInput, archivePath) {
|
|
const directory = resolve(bundleDirectoryInput);
|
|
const entries = listFiles(directory)
|
|
.map((filePath) => posixRelative(directory, filePath))
|
|
.sort(compareUtf8);
|
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
rmSync(archivePath, { force: true });
|
|
tar.c(
|
|
{
|
|
cwd: directory,
|
|
file: archivePath,
|
|
sync: true,
|
|
portable: true,
|
|
noPax: true,
|
|
mtime: new Date(0),
|
|
},
|
|
entries,
|
|
);
|
|
}
|
|
|
|
function importRuntimeArchive(archivePath, options) {
|
|
if (!existsSync(archivePath)) {
|
|
throw new Error(`Runtime artifact archive is missing: ${archivePath}`);
|
|
}
|
|
const projectRoot = options.projectRoot ?? root;
|
|
const architecture = options.architecture;
|
|
const outputRoot = options.outputRoot ?? defaultOutputRoot(projectRoot);
|
|
mkdirSync(outputRoot, { recursive: true });
|
|
const staging = mkdtempSync(
|
|
join(outputRoot, `.import-${targetName(architecture)}-`),
|
|
);
|
|
try {
|
|
tar.x({
|
|
cwd: staging,
|
|
file: archivePath,
|
|
sync: true,
|
|
strict: true,
|
|
preservePaths: false,
|
|
});
|
|
const verified = verifyBundleDirectory(staging, {
|
|
...options,
|
|
projectRoot,
|
|
architecture,
|
|
requireDigestDirectory: false,
|
|
});
|
|
const destination = bundleDirectory(
|
|
outputRoot,
|
|
architecture,
|
|
verified.manifest.bundleDigest,
|
|
);
|
|
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
if (existsSync(destination)) {
|
|
return verifyBundleDirectory(destination, {
|
|
...options,
|
|
projectRoot,
|
|
architecture,
|
|
});
|
|
}
|
|
chmodSync(staging, 0o700);
|
|
renameSync(staging, destination);
|
|
return verifyBundleDirectory(destination, {
|
|
...options,
|
|
projectRoot,
|
|
architecture,
|
|
});
|
|
} finally {
|
|
rmSync(staging, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function findOnlyBundle(outputRoot, architecture) {
|
|
const runtimeRoot = join(outputRoot, targetName(architecture), "opencode");
|
|
const entries = existsSync(runtimeRoot)
|
|
? readdirSync(runtimeRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => join(runtimeRoot, entry.name))
|
|
: [];
|
|
if (entries.length !== 1) {
|
|
throw new Error(
|
|
`Expected exactly one built Runtime bundle for ${targetName(architecture)}`,
|
|
);
|
|
}
|
|
return entries[0];
|
|
}
|
|
|
|
function redactSecrets(value, environment = process.env) {
|
|
let output = String(value);
|
|
for (const secret of [
|
|
environment[signingPrivateKeyEnvironment],
|
|
environment[signingKeyIdEnvironment],
|
|
]) {
|
|
if (secret && secret.length >= 4) {
|
|
output = output.split(secret).join("[redacted]");
|
|
}
|
|
}
|
|
return output.replace(
|
|
/-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/gu,
|
|
"[redacted private key]",
|
|
);
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const command = argv[0];
|
|
const options = {};
|
|
const allowed = new Set([
|
|
"--arch",
|
|
"--runtime-archive",
|
|
"--archive",
|
|
"--output-root",
|
|
"--bundle",
|
|
]);
|
|
for (let index = 1; index < argv.length; index += 2) {
|
|
const flag = argv[index];
|
|
const value = argv[index + 1];
|
|
if (!allowed.has(flag) || !value) {
|
|
throw new Error("Invalid Remote Runtime bundle argument");
|
|
}
|
|
options[
|
|
flag
|
|
.slice(2)
|
|
.replace(/-([a-z])/gu, (_match, value) => value.toUpperCase())
|
|
] = value;
|
|
}
|
|
if (!["build", "import", "verify", "preflight"].includes(command)) {
|
|
throw new Error(
|
|
"Usage: remote-runtime-bundle.cjs <build|import|verify|preflight> --arch <x64|arm64>",
|
|
);
|
|
}
|
|
if (command === "preflight") {
|
|
return { command };
|
|
}
|
|
targetName(options.arch);
|
|
return { command, ...options };
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const options = parseArguments(argv);
|
|
if (options.command === "preflight") {
|
|
const record = preflightProductionSigningKey();
|
|
console.log(
|
|
`Production GoodBuddy signing preflight passed for bundled Runtime: ${record.keyId}`,
|
|
);
|
|
return;
|
|
}
|
|
const outputRoot = options.outputRoot
|
|
? resolve(options.outputRoot)
|
|
: defaultOutputRoot();
|
|
if (options.command === "build") {
|
|
const verified = buildRuntimeBundle({
|
|
architecture: options.arch,
|
|
runtimeArchive: resolve(options.runtimeArchive ?? ""),
|
|
outputRoot,
|
|
});
|
|
const archivePath = options.archive
|
|
? resolve(options.archive)
|
|
: join(
|
|
root,
|
|
"dist",
|
|
"remote-runtime-bundles",
|
|
`goodbuddy-opencode-${targetName(options.arch)}.tar`,
|
|
);
|
|
createRuntimeArchive(verified.bundleDirectory, archivePath);
|
|
console.log(
|
|
`Remote Runtime bundle built and verified: ${targetName(options.arch)}`,
|
|
);
|
|
return;
|
|
}
|
|
if (options.command === "import") {
|
|
importRuntimeArchive(resolve(options.archive ?? ""), {
|
|
architecture: options.arch,
|
|
outputRoot,
|
|
});
|
|
console.log(
|
|
`Remote Runtime bundle imported and verified: ${targetName(options.arch)}`,
|
|
);
|
|
return;
|
|
}
|
|
verifyBundleDirectory(
|
|
options.bundle
|
|
? resolve(options.bundle)
|
|
: findOnlyBundle(outputRoot, options.arch),
|
|
{ architecture: options.arch },
|
|
);
|
|
console.log(`Remote Runtime bundle verified: ${targetName(options.arch)}`);
|
|
}
|
|
|
|
module.exports = {
|
|
buildRuntimeBundle,
|
|
bundleDirectory,
|
|
canonicalJson,
|
|
canonicalManifestBytes,
|
|
createManifest,
|
|
createRuntimeArchive,
|
|
defaultOutputRoot,
|
|
digestRuntimeBundleIdentity,
|
|
digestRuntimeBundleManifest,
|
|
fixedLimits,
|
|
importRuntimeArchive,
|
|
preflightProductionSigningKey,
|
|
profileDigests,
|
|
publicKeySpkiBase64,
|
|
readRemoteRuntimeLock,
|
|
readTrustedKeyRegistry,
|
|
redactSecrets,
|
|
resolveLockedRuntimeInput,
|
|
signaturePayload,
|
|
supportedArchitectures,
|
|
targetName,
|
|
verifyBundleDirectory,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(
|
|
redactSecrets(error instanceof Error ? error.message : String(error)),
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|