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 和用户主动联网请求。
222 lines
7.3 KiB
TypeScript
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)
|
|
);
|
|
}
|