mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
Merge refreshed Storybook layer into runtime hardening
Preserve managed OpenCode credential isolation while keeping the sandbox work-folder HOME. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
57c2f528f1
294 files changed
+115930
-1966
No files matched your search
@@ -126,7 +126,7 @@ describe("opencode remote execution", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prepares the workspace, syncs OpenCode skills, and restores workspace changes for remote SSH execution", async () => {
|
||||
it.each([false, true])("prepares the workspace, syncs OpenCode skills, and restores workspace changes for remote SSH execution (managed=%s)", async (managed) => {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-remote-"));
|
||||
cleanupDirs.push(rootDir);
|
||||
const workspaceDir = path.join(rootDir, "workspace");
|
||||
@@ -153,6 +153,13 @@ describe("opencode remote execution", () => {
|
||||
config: {
|
||||
command: "opencode",
|
||||
model: "opencode/gpt-5-nano",
|
||||
...(managed ? {
|
||||
managedAiConnection: { provider: "openrouter", method: "api_key" },
|
||||
} : {}),
|
||||
env: {
|
||||
XDG_CONFIG_HOME: path.join(rootDir, "config"),
|
||||
...(managed ? { HOME: "/var/folders/qa-managed", XDG_DATA_HOME: "/var/folders/qa-managed/data" } : {}),
|
||||
},
|
||||
},
|
||||
context: {
|
||||
paperclipWorkspace: {
|
||||
@@ -233,6 +240,17 @@ describe("opencode remote execution", () => {
|
||||
| [string, string, string[], { env: Record<string, string>; remoteExecution?: { remoteCwd: string } | null }]
|
||||
| undefined;
|
||||
expect(call?.[3].env.PAPERCLIP_WORKSPACE_CWD).toBe(managedRemoteWorkspace);
|
||||
if (managed) {
|
||||
const home = `${managedRemoteWorkspace}/.paperclip-runtime/opencode/managed-auth/run-1`;
|
||||
expect(call?.[3].env.HOME).toBe(home);
|
||||
expect(call?.[3].env.XDG_DATA_HOME).toBe(`${home}/data`);
|
||||
expect(modelProbeCall?.[3].env.XDG_DATA_HOME).toBe(`${home}/data`);
|
||||
expect(runSshCommand).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.stringContaining(`${home}/.claude/skills`),
|
||||
expect.anything(),
|
||||
);
|
||||
}
|
||||
expect(JSON.parse(call?.[3].env.PAPERCLIP_WORKSPACES_JSON ?? "[]")).toEqual([
|
||||
{
|
||||
workspaceId: "workspace-1",
|
||||
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
requireOpenCodeModelId,
|
||||
} from "./models.js";
|
||||
import { removeMaintainerOnlySkillSymlinks } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
import { prepareOpenCodeRuntimeConfig, prepareManagedOpenCodeRemoteHomes } from "./runtime-config.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { resolveOpenCodeSkillsHome } from "./skills.js";
|
||||
|
||||
@@ -440,8 +440,18 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
if (localRuntimeConfigHome && preparedExecutionTargetRuntime.assetDirs.xdgConfig) {
|
||||
preparedRuntimeConfig.env.XDG_CONFIG_HOME = preparedExecutionTargetRuntime.assetDirs.xdgConfig;
|
||||
}
|
||||
const remoteHomeDir = managedRemoteHomeDir
|
||||
?? await readAdapterExecutionTargetHomeDir(runId, executionTarget, {
|
||||
prepareManagedOpenCodeRemoteHomes({
|
||||
env: preparedRuntimeConfig.env,
|
||||
config,
|
||||
runtimeRootDir: preparedExecutionTargetRuntime.runtimeRootDir,
|
||||
runId,
|
||||
configDir: preparedExecutionTargetRuntime.assetDirs.xdgConfig,
|
||||
workFolderHome: executionTarget.transport === "sandbox" ? executionTarget.workFolderHome : undefined,
|
||||
});
|
||||
const remoteHomeDir = config.managedAiConnection
|
||||
? preparedRuntimeConfig.env.HOME
|
||||
: managedRemoteHomeDir
|
||||
?? await readAdapterExecutionTargetHomeDir(runId, executionTarget, {
|
||||
cwd,
|
||||
env: preparedRuntimeConfig.env,
|
||||
timeoutSec,
|
||||
|
||||
@@ -2,10 +2,43 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
import { prepareManagedOpenCodeRemoteHomes, prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
|
||||
const cleanupPaths = new Set<string>();
|
||||
|
||||
describe("prepareManagedOpenCodeRemoteHomes", () => {
|
||||
it("keeps the sandbox home while isolating managed credentials outside scoped folders", () => {
|
||||
const env = { HOME: "/host/private", XDG_DATA_HOME: "/host/data" };
|
||||
prepareManagedOpenCodeRemoteHomes({
|
||||
env,
|
||||
config: { managedAiConnection: true },
|
||||
runtimeRootDir: "/home/daytona/.paperclip-runtime/opencode",
|
||||
runId: "run-1",
|
||||
configDir: "/home/daytona/.paperclip-runtime/opencode/config",
|
||||
workFolderHome: "/home/daytona",
|
||||
});
|
||||
expect(env).toEqual({
|
||||
HOME: "/home/daytona",
|
||||
XDG_CONFIG_HOME: "/home/daytona/.paperclip-runtime/opencode/config",
|
||||
XDG_DATA_HOME: "/home/daytona/.paperclip-runtime/opencode/managed-auth/run-1/data",
|
||||
XDG_CACHE_HOME: "/home/daytona/.paperclip-runtime/opencode/managed-auth/run-1/cache",
|
||||
XDG_STATE_HOME: "/home/daytona/.paperclip-runtime/opencode/managed-auth/run-1/state",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the isolated remote home for execution without work folders", () => {
|
||||
const env: Record<string, string> = { HOME: "/host/private" };
|
||||
prepareManagedOpenCodeRemoteHomes({
|
||||
env,
|
||||
config: { managedAiConnection: true },
|
||||
runtimeRootDir: "/remote/runtime",
|
||||
runId: "run-1",
|
||||
});
|
||||
expect(env.HOME).toBe("/remote/runtime/managed-auth/run-1");
|
||||
expect(env.XDG_DATA_HOME).toBe("/remote/runtime/managed-auth/run-1/data");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
[...cleanupPaths].map(async (filepath) => {
|
||||
|
||||
@@ -240,3 +240,26 @@ export async function prepareOpenCodeRuntimeConfig(input: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Managed credentials must never leave host-only homes in a remote process. */
|
||||
export function prepareManagedOpenCodeRemoteHomes(input: {
|
||||
env: Record<string, string>;
|
||||
config: Record<string, unknown>;
|
||||
runtimeRootDir: string | null | undefined;
|
||||
runId: string;
|
||||
configDir?: string;
|
||||
workFolderHome?: string;
|
||||
}): void {
|
||||
if (!input.config.managedAiConnection) return;
|
||||
if (!input.runtimeRootDir) throw new Error("Managed OpenCode authentication requires an isolated remote runtime directory.");
|
||||
const home = path.posix.join(input.runtimeRootDir, "managed-auth", input.runId);
|
||||
Object.assign(input.env, {
|
||||
// Scoped work folders live beneath the sandbox's real home. OpenCode's
|
||||
// credential/configuration stores still use the isolated XDG directories.
|
||||
HOME: input.workFolderHome ?? home,
|
||||
XDG_CONFIG_HOME: input.configDir ?? path.posix.join(home, "config"),
|
||||
XDG_DATA_HOME: path.posix.join(home, "data"),
|
||||
XDG_CACHE_HOME: path.posix.join(home, "cache"),
|
||||
XDG_STATE_HOME: path.posix.join(home, "state"),
|
||||
});
|
||||
}
|
||||
@@ -74,6 +74,7 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async () => {
|
||||
import { testEnvironment } from "./test.js";
|
||||
|
||||
describe("opencode remote environment diagnostics", () => {
|
||||
const configHomes: string[] = [];
|
||||
let configHome: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -85,9 +86,12 @@ describe("opencode remote environment diagnostics", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
await rm(configHome, { recursive: true, force: true });
|
||||
await Promise.all(configHomes.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
it("stages remote runtime config assets for sandbox hello probes", async () => {
|
||||
it.each([false, true])("stages remote runtime config assets for sandbox hello probes (managed=%s)", async (managed) => {
|
||||
const configHome = await mkdtemp(path.join(os.tmpdir(), "opencode-remote-test-config-"));
|
||||
configHomes.push(configHome);
|
||||
const remoteTarget: AdapterExecutionTarget = {
|
||||
kind: "remote",
|
||||
transport: "sandbox",
|
||||
@@ -112,6 +116,13 @@ describe("opencode remote environment diagnostics", () => {
|
||||
config: {
|
||||
command: "opencode",
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
...(managed ? {
|
||||
managedAiConnection: { provider: "openrouter", method: "api_key" },
|
||||
} : {}),
|
||||
env: {
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
...(managed ? { OPENAI_API_KEY: "", OPENROUTER_API_KEY: "fixture", HOME: "/var/folders/qa-managed", XDG_DATA_HOME: "/var/folders/qa-managed/data" } : {}),
|
||||
},
|
||||
},
|
||||
executionTarget: remoteTarget,
|
||||
environmentName: "QA Cloudflare",
|
||||
@@ -134,6 +145,11 @@ describe("opencode remote environment diagnostics", () => {
|
||||
| [string, AdapterExecutionTarget, string, string[], { cwd: string; env: Record<string, string> }]
|
||||
| undefined;
|
||||
expect(probeCall?.[4].cwd).toBe("/remote/workspace/.paperclip-runtime/runs/test/workspace");
|
||||
if (managed) {
|
||||
expect(probeCall?.[4].env.HOME).toContain("/remote/workspace/.paperclip-runtime/runs/test/workspace/.paperclip-runtime/opencode/managed-auth/");
|
||||
expect(probeCall?.[4].env.XDG_DATA_HOME).toBe(`${probeCall?.[4].env.HOME}/data`);
|
||||
expect(probeCall?.[4].env.XDG_CACHE_HOME).toBe(`${probeCall?.[4].env.HOME}/cache`);
|
||||
}
|
||||
expect(probeCall?.[4].env.XDG_CONFIG_HOME).toBe(
|
||||
"/remote/workspace/.paperclip-runtime/runs/test/workspace/.paperclip-runtime/opencode/xdgConfig",
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||
import { parseOpenCodeJsonl } from "./parse.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
import { prepareOpenCodeRuntimeConfig, prepareManagedOpenCodeRemoteHomes } from "./runtime-config.js";
|
||||
|
||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||
if (checks.some((check) => check.level === "error")) return "fail";
|
||||
@@ -115,7 +115,7 @@ export async function testEnvironment(
|
||||
}
|
||||
|
||||
const openaiKeyOverride = "OPENAI_API_KEY" in envConfig ? asString(envConfig.OPENAI_API_KEY, "") : null;
|
||||
if (openaiKeyOverride !== null && openaiKeyOverride.trim() === "") {
|
||||
if (!config.managedAiConnection && openaiKeyOverride !== null && openaiKeyOverride.trim() === "") {
|
||||
checks.push({
|
||||
code: "opencode_openai_api_key_missing",
|
||||
level: "warn",
|
||||
@@ -172,6 +172,13 @@ export async function testEnvironment(
|
||||
if (localRuntimeConfigHome && preparedExecutionTargetRuntime.assetDirs.xdgConfig) {
|
||||
preparedRuntimeConfig.env.XDG_CONFIG_HOME = preparedExecutionTargetRuntime.assetDirs.xdgConfig;
|
||||
}
|
||||
prepareManagedOpenCodeRemoteHomes({
|
||||
env: preparedRuntimeConfig.env,
|
||||
config,
|
||||
runtimeRootDir: preparedExecutionTargetRuntime.runtimeRootDir,
|
||||
runId,
|
||||
configDir: preparedExecutionTargetRuntime.assetDirs.xdgConfig,
|
||||
});
|
||||
}
|
||||
const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...preparedRuntimeConfig.env }));
|
||||
|
||||
|
||||
Reference in new issue
Block a user