Files
goodbuddy/src/agent-daemon/opencode-runtime-profile.ts
T
mesalogo eb53c827c6 fix: apply runtime telemetry settings to web and remote clients
DS Web and remote Agent launch paths omitted shared telemetry opt-out settings. Reuse one environment definition for local launches, remote runtime profiles, and the remote model bridge helper while preserving existing update controls and user-requested networking.

Validation: 16 focused tests passed at commit time. Prior local real-model validation passed; modified Agent real-Host validation and background network monitoring remain outstanding.

Release note: 补齐 DS Web 与远程 Runtime 启动时的遥测关闭配置,保留现有自动更新关闭设置,不影响模型、MCP 和用户主动联网请求。
2026-09-27 13:57:26 +08:00

222 lines
7.3 KiB
TypeScript

import { runtimePrivacyEnvironment } from "../shared/runtime-privacy-environment";
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;
sharedSessions?: boolean;
};
}): OpenCodeLaunchProfile {
const manifest = remoteRuntimeBundleManifestSchema.parse(input.manifest);
const isContinue = manifest.runtimeId === "continue" && manifest.provider === "continue";
if (
!isContinue && (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 (isContinue && (manifest.entrypoint.identity !== "continue-acp" ||
manifest.entrypoint.path !== "lib/continue/dist/cn.js" || manifest.entrypoint.argvPrefix.length !== 0 || !input.modelBridge)) {
throw new Error("Continue requires its managed ACP facade and model bridge");
}
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),
sharedSessions: input.modelBridge.sharedSessions === true,
};
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,
...(modelBridge.sharedSessions ? ["--shared-sessions", "true"] : []),
...(isContinue ? ["--runtime-id", "continue"] : []),
isContinue ? "--continue-entrypoint" : "--opencode-entrypoint",
executablePath,
],
};
const environmentBase = {
...executeEnvironment(process.env, workspaceDirectory),
...runtimePrivacyEnvironment,
...(isContinue ? {} : { ...OPEN_CODE_RUNTIME_ENVIRONMENT,
OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode") }),
};
const permission = input.workMode === "ask" ? "ask" : "allow";
const environment = {
...environmentBase,
...(isContinue ? {} : { 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)
);
}