Files
goodbuddy/src/agent-daemon/opencode-runtime-profile.ts
T
mesalogo 5a77dfdb93
Cross-platform packages / Validate source (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 Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
fix: prevent managed runtime stalls during file changes
Automatic native Git snapshot diffs could block concurrent OpenCode sessions
after broad rewrites or line-ending conversions. Managed desktop and Agent
profiles now skip snapshots that GoodBuddy does not use, while preserving
file tools, native subagents, workspace Git operations, and Ask/Execute rules.

Drain otherwise-unused Continue utility-host stdout to prevent console output
from filling the pipe and blocking requests. Correct the lifecycle regression's
body observer to watch the underlying reader rather than a buffered wrapper.

Prepare Desktop 0.12.10 and Agent 0.11.22 with aligned bilingual notes and
feature documentation. Existing runtimes require restart or Agent update.
Full desktop UI before/after and Linux Agent production-path checks passed;
3626 tests passed, with 61 existing platform/manual skips.

Release note: 修复批量文件改写或换行转换后的托管 OpenCode 并行停顿,以及 Continue 控制台输出阻塞;本机升级重启后生效,远程项目请配套更新 Agent。
2026-09-09 03:07:55 +08:00

211 lines
6.4 KiB
TypeScript

import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import {
OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES,
remoteRuntimeBundleManifestSchema,
type RemoteRuntimeBundleManifest,
} from "../shared/remote-runtime-launch-contracts";
import {
modelBridgePolicySchema,
type ModelBridgePolicy,
} from "../shared/model-bridge-contracts";
import { assertAbsoluteManagedPath } from "./managed-paths";
const OPEN_CODE_RUNTIME_ENVIRONMENT = Object.freeze({
OPENCODE_DISABLE_AUTOUPDATE: "1",
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1",
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "1",
OPENCODE_DISABLE_LSP_DOWNLOAD: "1",
OPENCODE_DISABLE_MODELS_FETCH: "1",
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
OPENCODE_DISABLE_SHARE: "1",
});
export type OpenCodeLaunchProfile = {
executable: string;
processExecutable: string;
args: readonly string[];
cwd: string;
env: Readonly<NodeJS.ProcessEnv>;
workMode: "ask" | "execute";
};
export function createOpenCodeLaunchProfile(input: {
manifest: RemoteRuntimeBundleManifest;
bundleDirectory: string;
workspaceDirectory: string;
workMode: "ask" | "execute";
modelBridge?: {
agentExecutablePath: string;
bridgeDirectory: string;
socketPath: string;
policy: ModelBridgePolicy;
};
}): OpenCodeLaunchProfile {
const manifest = remoteRuntimeBundleManifestSchema.parse(input.manifest);
if (
manifest.runtimeId !== "opencode" ||
manifest.provider !== "opencode" ||
manifest.entrypoint.identity !== "opencode-acp" ||
manifest.entrypoint.argvPrefix.length !== 1 ||
manifest.entrypoint.argvPrefix[0] !== "acp"
) {
throw new Error("Runtime manifest is not the fixed OpenCode ACP profile");
}
if (
[...manifest.allowedEnvironmentNames].sort().join("\0") !==
[...OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES].sort().join("\0")
) {
throw new Error(
"OpenCode Runtime environment allowlist does not match the fixed profile",
);
}
const bundleDirectory = assertAbsoluteManagedPath(
resolve(input.bundleDirectory),
);
const workspaceDirectory = normalizedAbsolutePath(
input.workspaceDirectory,
"Workspace",
);
const modelBridge =
input.modelBridge === undefined
? undefined
: {
agentExecutablePath: normalizedAbsolutePath(
input.modelBridge.agentExecutablePath,
"Agent executable",
),
agentInstallationDirectory: normalizedAbsolutePath(
dirname(input.modelBridge.agentExecutablePath),
"Agent installation",
),
bridgeDirectory: normalizedAbsolutePath(
input.modelBridge.bridgeDirectory,
"Model bridge",
),
socketPath: normalizedAbsolutePath(
input.modelBridge.socketPath,
"Model bridge socket",
),
policy: modelBridgePolicySchema.parse(input.modelBridge.policy),
};
if (
modelBridge !== undefined &&
(!isStrictDescendant(modelBridge.bridgeDirectory, modelBridge.socketPath) ||
!isStrictDescendant(
modelBridge.agentInstallationDirectory,
modelBridge.agentExecutablePath,
))
) {
throw new Error(
"Model bridge socket or Agent executable escapes its managed directory",
);
}
const executablePath = resolve(
bundleDirectory,
...manifest.entrypoint.path.split("/"),
);
if (!isStrictDescendant(bundleDirectory, executablePath)) {
throw new Error("OpenCode entrypoint escapes its bundle directory");
}
const runtimeCommand =
modelBridge === undefined
? {
executable: executablePath,
args: [...manifest.entrypoint.argvPrefix],
}
: {
executable: join(modelBridge.agentInstallationDirectory, "node"),
args: [
join(modelBridge.agentInstallationDirectory, "lib", "agent.cjs"),
"model-bridge-helper",
"--socket-path",
modelBridge.socketPath,
"--protocol",
modelBridge.policy.protocol,
"--model",
modelBridge.policy.model,
"--supports-image-input",
modelBridge.policy.supportsImageInput ? "true" : "false",
"--work-mode",
input.workMode,
"--opencode-entrypoint",
executablePath,
],
};
const environmentBase = {
...executeEnvironment(process.env, workspaceDirectory),
...OPEN_CODE_RUNTIME_ENVIRONMENT,
OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode"),
};
const permission = input.workMode === "ask" ? "ask" : "allow";
const environment = {
...environmentBase,
OPENCODE_CONFIG_CONTENT: JSON.stringify({
// Native snapshot summaries are not consumed by GoodBuddy.
snapshot: false,
permission,
agent: { build: { permission } },
}),
};
return {
...runtimeCommand,
processExecutable: runtimeCommand.executable,
cwd: workspaceDirectory,
env: environment,
workMode: input.workMode,
};
}
function normalizedAbsolutePath(value: string, label: string): string {
if (value.includes("\0") || !isAbsolute(value) || resolve(value) !== value) {
throw new Error(`${label} path must be a normalized absolute path`);
}
return value;
}
function executeEnvironment(
source: Readonly<NodeJS.ProcessEnv>,
workspaceDirectory: string,
): Readonly<NodeJS.ProcessEnv> {
const home = usableEnvironmentValue(source.HOME)
? source.HOME!
: workspaceDirectory;
const defaults = {
HOME: home,
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8",
PATH: "/usr/bin:/bin",
TMPDIR: "/tmp",
XDG_CACHE_HOME: join(home, ".cache"),
XDG_CONFIG_HOME: join(home, ".config"),
XDG_DATA_HOME: join(home, ".local", "share"),
XDG_STATE_HOME: join(home, ".local", "state"),
} as const;
const environment: NodeJS.ProcessEnv = { ...source };
for (const [name, fallback] of Object.entries(defaults)) {
if (!usableEnvironmentValue(environment[name])) {
environment[name] = fallback;
}
}
return environment;
}
function usableEnvironmentValue(value: string | undefined): value is string {
return value !== undefined && value.length > 0 && !value.includes("\0");
}
function isStrictDescendant(parent: string, candidate: string): boolean {
const child = relative(parent, candidate);
return (
child.length > 0 &&
child !== ".." &&
!child.startsWith(`..${sep}`) &&
!isAbsolute(child)
);
}