mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(opencode-local): preserve the agent during sandbox tool OOMs (#17398)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Legacy OpenCode adapter runs an agent and its shell tools in the same sandbox. > - A large build can exceed the sandbox memory limit and trigger a kernel OOM kill. > - The kernel can kill OpenCode before its tool, which loses the active turn. > - OpenCode provides a shell environment hook that can change only the tool subprocess. > - This pull request uses that hook to prefer Bash tool processes as OOM victims. > - The benefit is that OpenCode can report a failed tool and continue the task. ## Linked Issues or Issue Description **What happened?** An OpenCode sandbox run ended with exit 137 after a TypeScript process had also been killed. Its cgroup limit was 4 GiB. It had completed useful tool work and had not reached its execution deadline. The original sandbox was deleted, so its historical OOM counters are unavailable. Memory pressure is the leading explanation, not a proven historical kernel diagnosis. **Expected behavior** When a shell tool exhausts sandbox memory, the kernel should prefer that tool over OpenCode where possible. The adapter must preserve configured shell startup, model, permissions and resource limits. **Steps to reproduce** Use a disposable 4 GiB Linux sandbox with OpenCode. Retain a 1,200 MiB buffer in the agent and run a Bash tool with fourteen 250 MiB workers. Compare the same workload before and after installing the generated hook. Record the cgroup memory events, agent exit and final response. Refs: #16844. That PR covers the same protection for Claude. This PR adds the OpenCode-owned hook. Refs: #17254 for the separate shutdown recovery path. This PR does not change retry policy. ## What Changed - Generate a dependency-free OpenCode `shell.env` plugin and a Bash startup file in the isolated runtime config. - Stage those assets only for sandbox execution. Resolve the startup file relative to the copied plugin. - Set child `oom_score_adj` to `1000` on Linux. Keep the parent environment unchanged and honor existing `BASH_ENV` values, including an empty value. - Keep command execution available if the optional plugin cannot be installed or the process priority cannot be changed. - Materialize linked plugin directories inside the isolated config before writing. Keep the operator directory and its plugins unchanged. - Cover relocation, shell output and exit status, operator settings, local execution, remote staging and failed installation. Update the adapter documentation and verification recipe. ## Verification - Red before the fix: the new runtime regression failed because the sandbox shell plugin was absent. The existing 15 runtime config tests passed. The same regression passes with the fix. - Apex first found a host-write risk through linked plugins directories (4/5). A new regression reproduced the host directory modification, then passed after the installer materialized the copied link. The finding is resolved. Apex reviewed current head `df82a8f51725a197c0ea3ebeb942488509f32b34` at 5/5 with no actionable issues remaining. - The full OpenCode adapter suite passes: 87 source tests. The adapter typecheck and map checks pass. - Real sandbox acceptance used the plugin generated from `df82a8f51725a197c0ea3ebeb942488509f32b34` and OpenCode 1.18.34 in a disposable 4 GiB Daytona sandbox. A controlled local model endpoint issued an actual Bash tool call; no real model credentials were used. The tool reported priority `1000`; the parent reported `0`. - Controlled OOM comparison: without the guard, OpenCode was killed by SIGKILL after 8.95 seconds and the cgroup recorded one OOM kill. With the guard, five workers were killed, OpenCode exited `0` after 21.87 seconds, and its final response was `fixture-complete`. The cgroup counter increased by five. Both attempts used the same allocation and workload. The fixture started with a linked operator plugins directory. The installer preserved that directory, staged a real plugins directory, and passed both the process-priority probe and OOM comparison. The fixture allocation was deleted afterward. - The first live probe was not accepted because the harness could not parse the provider response framing. After correcting the decoder, the unchanged plugin passed the probe and the controlled OOM comparison. Both initial disposable allocations were deleted. After the review fix, a third allocation repeated the probe and OOM comparison with a linked plugins directory. That allocation was also deleted. The initial comparison had the same agent-killed/agent-survived result (8.42 seconds without the guard; 19.42 seconds with it). - Repository `pnpm -r typecheck` and `pnpm build` pass. The full CI matrix passes on `df82a8f51725a197c0ea3ebeb942488509f32b34`, including tests, browser shards, Runner checks, build, policy and Apex. [CI run](https://github.com/paperclipai/paperclip/actions/runs/40043947375). - The first full local test attempt had 15 embedded PostgreSQL startup failures. Direct `initdb --version` and `postgres --version` probes showed missing bundled library symlinks and SIGABRT. Running the bundled symlink hydration script restored both executables. All 15 affected cases pass after that setup repair. No product code changed for it. - Two existing workspace tests assumed that new Git repositories have no `main` branch. They failed under the host default branch setting of `main`, then passed with a per-command `init.defaultBranch=master` setting. The global setting was preserved. The full local test command was restarted with hydrated libraries and that controlled fixture default. It had no reported assertion failures when it was stopped after the complete CI matrix passed. It did not complete locally; full-suite verification comes from CI. The first failed invocation was also stopped. - The original tenant task has not been retried or recovered by this PR. Its board API credential is expired. The original hosted workflow and the separate shutdown recovery remain outside this acceptance result. ## Risks - This is a preference, not a memory reservation. It cannot guarantee that OpenCode survives every OOM condition, including memory growth inside OpenCode itself. - The guard applies to non-interactive Bash tools. Other shells ignore `BASH_ENV`. Existing operator startup settings take precedence. - Older OpenCode releases without the shell environment hook will not apply the preference. No model, permission, deadline, memory quota or retry limit changes. - No schema change or migration. ## Model Used OpenAI GPT-6 through Codex, with repository tools and code execution. The exact deployment variant and context window are not exposed by this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a978ecadef
commit
724fa6144d
7 files changed
+219
-2
No files matched your search
@@ -53,6 +53,19 @@ Worked examples:
|
||||
`credentials.json` from the sandbox image's own `$HOME/.claude`. The
|
||||
snapshot's Claude login is the credential source for the run.
|
||||
|
||||
### OpenCode sandbox memory pressure
|
||||
|
||||
On managed Linux sandboxes, the Legacy OpenCode adapter adds a local plugin through
|
||||
OpenCode's [`shell.env` hook](https://opencode.ai/docs/plugins/#inject-environment-variables).
|
||||
The hook supplies a Bash startup file only to shell subprocesses. It sets their
|
||||
Linux `oom_score_adj` to `1000`, so the kernel prefers to kill memory-heavy tool
|
||||
commands before OpenCode when the sandbox runs out of memory.
|
||||
|
||||
An existing `BASH_ENV` takes precedence. Local and SSH execution keep their
|
||||
existing behavior. The guard does not increase the sandbox memory limit, apply
|
||||
to other shells, or prevent OpenCode itself from exhausting memory. If the
|
||||
runtime cannot set the process preference, the command still runs.
|
||||
|
||||
### Hermes local vs gateway
|
||||
|
||||
Use `hermes_local` when Paperclip should start the local `hermes` CLI on the
|
||||
|
||||
@@ -178,6 +178,17 @@ Automated: [live run routes](../../server/src/__tests__/agent-live-run-routes.te
|
||||
|
||||
Manual: Start a disposable task, open its running transcript, and follow it to a terminal outcome. Reload and compare task/run IDs, status, output, and cost. Verify a failed run remains distinguishable from a completed run with useful output.
|
||||
|
||||
For Legacy OpenCode on a disposable Linux sandbox, run a Bash tool that reads
|
||||
`/proc/self/oom_score_adj` and its parent's score. The tool should report `1000`;
|
||||
OpenCode should retain its original score. Repeat with an operator `BASH_ENV`
|
||||
and confirm that it takes precedence. Repeat with a linked operator plugins
|
||||
directory; it must remain unchanged, and the uploaded runtime must contain a
|
||||
real plugins directory with the guard and existing plugins. Under controlled memory pressure, record
|
||||
the cgroup OOM counters and verify that a killed tool can return an error while
|
||||
the agent continues. Local and SSH execution must keep their original shell
|
||||
environment. See the [adapter contract](../../docs/adapters/overview.md#opencode-sandbox-memory-pressure).
|
||||
These controls do not guarantee that every memory exhaustion preserves the agent.
|
||||
|
||||
An explicit runner change starts a configuration revision and invalidates the
|
||||
eligible session. Active runs, restart recovery and historical transcript parsing
|
||||
retain their recorded execution configuration; do not infer their runner from a
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async (importOriginal) =>
|
||||
runAdapterExecutionTargetProcess: mocks.process,
|
||||
}));
|
||||
import { execute } from "./execute.js";
|
||||
import { prepareAdapterExecutionTargetRuntime } from "@paperclipai/adapter-utils/execution-target";
|
||||
|
||||
const rawOutput = "private-output-canary";
|
||||
const rawError = "<html>504 Gateway Time-out private-provider-token-canary</html>";
|
||||
@@ -80,6 +81,21 @@ describe("OpenCode timeout reporting", () => {
|
||||
expect(mocks.stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uploads the sandbox shell plugin and launches with its remote config home", async () => {
|
||||
vi.mocked(prepareAdapterExecutionTargetRuntime).mockImplementationOnce(async (input) => {
|
||||
const asset = input.assets?.find(asset => asset.key === "xdgConfig");
|
||||
expect(asset).toBeDefined();
|
||||
const plugins = await fs.readdir(path.join(asset!.localDir, "opencode", "plugins"));
|
||||
expect(plugins).toHaveLength(1);
|
||||
return { target: input.target!, additionalSourceDirs: {}, additionalSourceFailures: [], workspaceSyncSnapshot: null,
|
||||
runtimeRootDir: "/workspace/runtime", workspaceRemoteDir: "/workspace",
|
||||
assetDirs: { xdgConfig: "/workspace/runtime/config" }, restoreWorkspace: mocks.restore };
|
||||
});
|
||||
await execute(context(0));
|
||||
expect(mocks.process.mock.calls[0]![4].env.XDG_CONFIG_HOME).toBe("/workspace/runtime/config");
|
||||
expect(mocks.process.mock.calls[0]![4].env).not.toHaveProperty("BASH_ENV");
|
||||
});
|
||||
|
||||
it("runs the sandbox CLI through the actual process seam with session dispatch and incremental output", async () => {
|
||||
const actual = await vi.importActual<typeof import("@paperclipai/adapter-utils/execution-target")>("@paperclipai/adapter-utils/execution-target");
|
||||
mocks.process.mockImplementation(actual.runAdapterExecutionTargetProcess);
|
||||
|
||||
@@ -330,7 +330,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
if (authToken) {
|
||||
env.PAPERCLIP_API_KEY = authToken;
|
||||
}
|
||||
const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({ env, config });
|
||||
const preparedRuntimeConfig = await prepareOpenCodeRuntimeConfig({
|
||||
env, config,
|
||||
sandboxToolOomPreference: executionTarget?.kind === "remote" && executionTarget.transport === "sandbox",
|
||||
});
|
||||
const localRuntimeConfigHome =
|
||||
preparedRuntimeConfig.notes.length > 0 ? preparedRuntimeConfig.env.XDG_CONFIG_HOME : "";
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { OPENROUTER_ATTRIBUTION_HEADERS } from "@paperclipai/adapter-utils/openrouter-attribution";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
|
||||
@@ -32,6 +34,115 @@ async function makeConfigHome(initialConfig?: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
describe("prepareOpenCodeRuntimeConfig", () => {
|
||||
it("stages a relocatable sandbox shell hook without changing the parent environment", async () => {
|
||||
const configHome = await makeConfigHome({ plugin: ["operator-plugin"], permission: { bash: "ask" } });
|
||||
const input = { env: { XDG_CONFIG_HOME: configHome }, config: { dangerouslySkipPermissions: false }, sandboxToolOomPreference: true };
|
||||
const prepared = await prepareOpenCodeRuntimeConfig(input);
|
||||
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
|
||||
const copiedHome = await makeConfigHome();
|
||||
const relocated = path.join(copiedHome, "remote config 'with spaces'");
|
||||
await fs.cp(prepared.env.XDG_CONFIG_HOME, relocated, { recursive: true });
|
||||
await prepared.cleanup();
|
||||
const pluginDir = path.join(relocated, "opencode", "plugins");
|
||||
const plugins = await fs.readdir(pluginDir);
|
||||
expect(plugins).toHaveLength(1);
|
||||
const pluginUrl = pathToFileURL(path.join(pluginDir, plugins[0]!)).href;
|
||||
const hookEnv = JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e", `
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
delete process.env.BASH_ENV;
|
||||
const hooks = await Object.values(await import(${JSON.stringify(pluginUrl)}))[0]();
|
||||
const output = { env: {} };
|
||||
await hooks["shell.env"]({}, output);
|
||||
console.log(JSON.stringify({ child: output.env, parent: process.env.BASH_ENV ?? null }));
|
||||
`], { encoding: "utf8" }));
|
||||
expect(hookEnv.parent).toBeNull();
|
||||
expect(hookEnv.child.BASH_ENV).toContain(relocated);
|
||||
const command = spawnSync("bash", ["-c", 'printf "%s" "$1"; exit 7', "bash", "literal ' $() value"], {
|
||||
env: { ...process.env, ...hookEnv.child }, encoding: "utf8",
|
||||
});
|
||||
expect(command.stdout).toBe("literal ' $() value");
|
||||
expect(command.status).toBe(7);
|
||||
if (process.platform === "linux") {
|
||||
const parentScore = await fs.readFile("/proc/self/oom_score_adj", "utf8");
|
||||
expect(execFileSync("bash", ["-c", "cat /proc/self/oom_score_adj"], {
|
||||
env: { ...process.env, ...hookEnv.child }, encoding: "utf8",
|
||||
}).trim()).toBe("1000");
|
||||
expect(await fs.readFile("/proc/self/oom_score_adj", "utf8")).toBe(parentScore);
|
||||
}
|
||||
const config = JSON.parse(await fs.readFile(path.join(relocated, "opencode", "opencode.json"), "utf8"));
|
||||
expect(config).toMatchObject({ plugin: ["operator-plugin"], permission: { bash: "ask" } });
|
||||
expect(prepared.env).not.toHaveProperty("BASH_ENV");
|
||||
});
|
||||
|
||||
it("preserves operator shell setup and leaves other platforms and local runs alone", async () => {
|
||||
const configHome = await makeConfigHome();
|
||||
const input = { env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true };
|
||||
const prepared = await prepareOpenCodeRuntimeConfig(input);
|
||||
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
|
||||
const pluginDir = path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "plugins");
|
||||
const pluginUrl = pathToFileURL(path.join(pluginDir, (await fs.readdir(pluginDir))[0]!)).href;
|
||||
for (const scenario of [
|
||||
{ platform: "linux", parent: "/operator/setup.sh", child: {} },
|
||||
{ platform: "linux", parent: "", child: {} },
|
||||
{ platform: "linux", parent: null, child: { BASH_ENV: "/other-plugin/setup.sh" } },
|
||||
{ platform: "darwin", parent: null, child: {} },
|
||||
]) {
|
||||
const result = JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e", `
|
||||
const scenario = ${JSON.stringify(scenario)};
|
||||
Object.defineProperty(process, "platform", { value: scenario.platform });
|
||||
if (scenario.parent === null) delete process.env.BASH_ENV;
|
||||
else process.env.BASH_ENV = scenario.parent;
|
||||
const hooks = await Object.values(await import(${JSON.stringify(pluginUrl)}))[0]();
|
||||
const output = { env: scenario.child };
|
||||
await hooks["shell.env"]({}, output);
|
||||
console.log(JSON.stringify(output.env));
|
||||
`], { encoding: "utf8" }));
|
||||
expect(result).toEqual(scenario.child);
|
||||
}
|
||||
const local = await prepareOpenCodeRuntimeConfig({ env: { XDG_CONFIG_HOME: configHome }, config: {} });
|
||||
cleanupPaths.add(local.env.XDG_CONFIG_HOME);
|
||||
expect(await fs.readdir(path.join(local.env.XDG_CONFIG_HOME, "opencode"))).toEqual(["opencode.json"]);
|
||||
});
|
||||
|
||||
it("keeps the existing runtime usable when the optional plugin cannot be installed", async () => {
|
||||
const configHome = await makeConfigHome({ theme: "system" });
|
||||
const rename = vi.spyOn(fs, "rename").mockRejectedValueOnce(new Error("fixture write denied"));
|
||||
try {
|
||||
const input = { env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true };
|
||||
const prepared = await prepareOpenCodeRuntimeConfig(input);
|
||||
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
|
||||
const dir = path.join(prepared.env.XDG_CONFIG_HOME, "opencode");
|
||||
expect(await fs.readdir(path.join(dir, "plugins"))).toEqual([]);
|
||||
expect(await fs.readdir(dir)).toEqual(["opencode.json", "plugins"]);
|
||||
expect(prepared.notes.some(note => note.startsWith("Could not prepare"))).toBe(true);
|
||||
expect(JSON.parse(await fs.readFile(path.join(dir, "opencode.json"), "utf8"))).toMatchObject({ theme: "system" });
|
||||
} finally {
|
||||
rename.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("materializes linked plugin directories without writing into the operator configuration", async () => {
|
||||
const configHome = await makeConfigHome();
|
||||
const operatorRoot = await makeConfigHome();
|
||||
const operatorPlugins = path.join(operatorRoot, "plugins");
|
||||
await fs.mkdir(operatorPlugins);
|
||||
const operatorPlugin = "export const Operator = async () => ({});\n";
|
||||
await fs.writeFile(path.join(operatorPlugins, "operator.js"), operatorPlugin);
|
||||
await fs.symlink(operatorPlugins, path.join(configHome, "opencode", "plugins"), "dir");
|
||||
const prepared = await prepareOpenCodeRuntimeConfig({
|
||||
env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true,
|
||||
});
|
||||
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
|
||||
expect(await fs.readdir(operatorPlugins)).toEqual(["operator.js"]);
|
||||
const plugins = path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "plugins");
|
||||
expect((await fs.lstat(plugins)).isDirectory()).toBe(true);
|
||||
expect(await fs.readdir(plugins)).toHaveLength(2);
|
||||
expect(await fs.readFile(path.join(plugins, "operator.js"), "utf8")).toBe(operatorPlugin);
|
||||
await prepared.cleanup();
|
||||
expect(await fs.readdir(operatorPlugins)).toEqual(["operator.js"]);
|
||||
expect((await fs.lstat(path.join(configHome, "opencode", "plugins"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
it("attributes custom OpenRouter aliases and inline overrides without changing other endpoints", async () => {
|
||||
const other = { options: { baseURL: "https://other.example/v1", headers: { "X-Title": "Other" } } };
|
||||
const configHome = await makeConfigHome({ provider: { other, openrouter: other } });
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { asBoolean } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { installSandboxToolMemoryPreference } from "./sandbox-tool-memory.js";
|
||||
|
||||
type PreparedOpenCodeRuntimeConfig = {
|
||||
env: Record<string, string>;
|
||||
@@ -107,6 +108,7 @@ export async function prepareOpenCodeRuntimeConfig(input: {
|
||||
env: Record<string, string>;
|
||||
config: Record<string, unknown>;
|
||||
targetIsRemote?: boolean;
|
||||
sandboxToolOomPreference?: boolean;
|
||||
}): Promise<PreparedOpenCodeRuntimeConfig> {
|
||||
const skipPermissions = asBoolean(input.config.dangerouslySkipPermissions, true);
|
||||
|
||||
@@ -228,6 +230,14 @@ export async function prepareOpenCodeRuntimeConfig(input: {
|
||||
notes.push(`Pinned OpenCode small_model to ${smallModel}.`);
|
||||
}
|
||||
await fs.writeFile(runtimeConfigPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
||||
if (input.sandboxToolOomPreference) {
|
||||
try {
|
||||
await installSandboxToolMemoryPreference(runtimeConfigDir);
|
||||
notes.push("Prepared sandbox Bash tool memory preference; shell commands are preferred OOM victims over OpenCode.");
|
||||
} catch {
|
||||
notes.push("Could not prepare sandbox Bash tool memory preference; continuing with the existing shell behavior.");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
env: {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
async function preparePluginDirectory(configDir: string, pluginDir: string): Promise<void> {
|
||||
const stat = await fs.lstat(pluginDir).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!stat?.isSymbolicLink()) {
|
||||
await fs.mkdir(pluginDir, { recursive: true, mode: 0o700 });
|
||||
return;
|
||||
}
|
||||
const linkTarget = await fs.readlink(pluginDir);
|
||||
const detached = await fs.mkdtemp(path.join(configDir, "paperclip-plugins-"));
|
||||
try {
|
||||
await fs.cp(await fs.realpath(pluginDir), detached, { recursive: true, dereference: false });
|
||||
await fs.unlink(pluginDir);
|
||||
try {
|
||||
await fs.rename(detached, pluginDir);
|
||||
} catch (error) {
|
||||
await fs.symlink(linkTarget, pluginDir, "dir");
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(detached, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function installSandboxToolMemoryPreference(configDir: string): Promise<void> {
|
||||
const supportDir = await fs.mkdtemp(path.join(configDir, "paperclip-tool-memory-"));
|
||||
const pluginDir = path.join(configDir, "plugins");
|
||||
const pluginPath = path.join(pluginDir, `${path.basename(supportDir)}.js`);
|
||||
const partialPath = `${pluginPath}.partial`;
|
||||
try {
|
||||
await fs.writeFile(path.join(supportDir, "bash-env.sh"),
|
||||
"{ printf '1000\\n' > /proc/self/oom_score_adj; } 2>/dev/null || true\n", { mode: 0o600 });
|
||||
await preparePluginDirectory(configDir, pluginDir);
|
||||
const scriptUrl = `../${path.basename(supportDir)}/bash-env.sh`;
|
||||
await fs.writeFile(partialPath, `import { fileURLToPath } from "node:url";
|
||||
export const PaperclipSandboxToolMemory = async () => ({
|
||||
"shell.env": async (_input, output) => {
|
||||
if (process.platform !== "linux" || "BASH_ENV" in process.env || "BASH_ENV" in output.env) return;
|
||||
output.env.BASH_ENV = fileURLToPath(new URL(${JSON.stringify(scriptUrl)}, import.meta.url));
|
||||
},
|
||||
});
|
||||
`, { mode: 0o600 });
|
||||
await fs.rename(partialPath, pluginPath);
|
||||
} catch (error) {
|
||||
await fs.rm(partialPath, { force: true }).catch(() => {});
|
||||
await fs.rm(supportDir, { recursive: true, force: true }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user