Refresh run config before reusing workspaces (#8797)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent runs are assembled by the heartbeat service from agent config,
project workspaces, environment config, secret bindings, skills, and
runtime session state.
> - The heartbeat service intentionally reuses adapter sessions,
execution workspaces, and sandbox leases when that preserves useful
state.
> - Reuse becomes incorrect when the effective next-run config changes
after a saved session, workspace, or lease was created.
> - Stale reuse can make a later run appear pinned to old agent,
environment, secret, instruction, or workspace settings.
> - This pull request records non-sensitive fingerprints for the
effective session, workspace, and lease config at run boundaries.
> - When those fingerprints drift, Paperclip refreshes persisted runtime
config or starts fresh execution instead of reusing stale state.
> - The benefit is predictable next-run config freshness without storing
raw secret values, full env maps, provider credentials, or private path
details.

## Linked Issues or Issue Description

- Refs #8058
- Related PRs checked during dedup search: #4968, #4155, #84, #8480.
These cover nearby workspace/session routing or model-config freshness
areas, but do not duplicate this effective run config fingerprinting
path.

## What Changed

- Added effective run config fingerprinting for session, workspace, and
lease reuse decisions, with canonicalization that ignores generated
runtime noise and redacts sensitive values.
- Updated heartbeat reuse logic to compare stored and next-run
fingerprints, reset stale saved sessions, refresh persisted workspace
config snapshots, replace stale reused workspaces when required, and
avoid stale sandbox lease reuse.
- Included plain environment value drift via value hashes, without
storing the raw env values.
- Root-bound instruction content hashing so legacy direct absolute
instruction paths are represented but not read for config fingerprints.
- Batched secret/version metadata lookups for environment lease
fingerprinting.
- Added workspace operation/run result freshness metadata so operators
can inspect non-sensitive decision categories.
- Surfaced config freshness labels and next-run copy in the UI and docs.
- Added focused coverage for fingerprint redaction, session reset
decisions, workspace refresh/replace behavior, environment lease drift,
and persisted workspace restoration.

## Verification

- `git diff --check`
- Sensitive-data scan before push:
- `git diff --unified=0 origin/master...HEAD | rg -n --pcre2
"(AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9]{20,}|-----BEGIN
(RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----|AKIA[0-9A-Z]{16})"`
- `git diff --unified=0 origin/master...HEAD | rg -n --pcre2
"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"`
- `pnpm exec vitest run
server/src/__tests__/effective-run-config-fingerprints.test.ts
server/src/__tests__/heartbeat-workspace-session.test.ts
server/src/__tests__/environment-runtime.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm -r typecheck`
- `pnpm --filter @paperclipai/db clean`
- `pnpm test:run`
- `pnpm build`
- UI screenshots from Cutter:
-
https://artifacts.cutter.sh/8797/run-2f4827c-2026-06-30T18-57-25/preview/change-01.png
-
https://artifacts.cutter.sh/8797/run-2f4827c-2026-06-30T18-57-25/preview/change-02.png
-
https://artifacts.cutter.sh/8797/run-2f4827c-2026-06-30T18-57-25/preview/change-03.png

## Risks

- Medium: overly broad fingerprints could start fresh sessions,
workspaces, or sandbox leases more often than necessary.
- Medium: missing a config category would allow stale reuse to persist
for that category.
- Medium: legacy direct absolute instruction paths are no longer
content-hashed unless they are paired with an absolute managed
instructions root.
- Low data risk: fingerprint metadata stores hashes and category names,
not raw secrets, raw env values, provider credentials, or private path
details.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI GPT-5 via Codex CLI / Codex coding agent, tool-enabled with
shell, Git, GitHub CLI, local test execution, and code editing. The
exact deployed model variant and context window are not exposed by this
environment.

## 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>
Co-authored-by: Cody <cody@paperclip.ing>
This commit is contained in:
authored and GitHub committed 2026-06-30 14:50:25 -07:00
1 parent 5bd6c6ec3c
commit a8f0ebaa80
15 files changed
+3325 -249

No files matched your search

@@ -0,0 +1,303 @@
import { describe, expect, it } from "vitest";
import {
canonicalizeEffectiveRunConfigCategory,
createEffectiveRunConfigFingerprints,
diffEffectiveRunConfigFingerprints,
} from "../services/effective-run-config-fingerprints.ts";
describe("effective run config fingerprints", () => {
it("emits versioned deterministic fingerprints with stable object ordering", () => {
const first = createEffectiveRunConfigFingerprints({
session: {
adapterType: "codex_local",
adapterConfig: {
env: {
ZETA: "raw-zeta",
ALPHA: "raw-alpha",
PAPERCLIP_RUN_ID: "run-one",
},
model: "gpt-5.2",
},
},
workspace: {
repoRef: "main",
repoUrl: "https://github.com/example/repo.git",
workspaceRuntime: {
services: [
{
env: { PORT: "3100", FEATURE_FLAG: "enabled" },
command: "pnpm dev",
},
],
},
},
lease: {
environmentId: "environment-1",
config: { beta: true, alpha: 1 },
},
});
const second = createEffectiveRunConfigFingerprints({
lease: {
config: { alpha: 1, beta: true },
environmentId: "environment-1",
},
workspace: {
workspaceRuntime: {
services: [
{
command: "pnpm dev",
env: { FEATURE_FLAG: "enabled", PORT: "3100" },
},
],
},
repoUrl: "https://github.com/example/repo.git",
repoRef: "main",
},
session: {
adapterConfig: {
model: "gpt-5.2",
env: {
PAPERCLIP_RUN_ID: "run-two",
ALPHA: "raw-alpha",
ZETA: "raw-zeta",
},
},
adapterType: "codex_local",
},
});
expect(first.version).toBe(1);
expect(first.categories).toEqual(["session", "workspace", "lease"]);
expect(first.sessionFingerprint).toMatchObject({
version: 1,
category: "session",
algorithm: "sha256",
});
expect(first.sessionFingerprint.fingerprint).toMatch(/^v1:sha256:[a-f0-9]{64}$/);
expect(second).toEqual(first);
});
it("reports changed categories independently", () => {
const base = createEffectiveRunConfigFingerprints({
session: { adapterType: "codex_local", model: "gpt-5.2" },
workspace: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}" } },
lease: { environmentId: "environment-1", reuseLease: true },
});
expect(diffEffectiveRunConfigFingerprints(
base,
createEffectiveRunConfigFingerprints({
session: { adapterType: "codex_local", model: "gpt-5.3" },
workspace: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}" } },
lease: { environmentId: "environment-1", reuseLease: true },
}),
)).toMatchObject({
hasChanges: true,
changedCategories: ["session"],
changed: { session: true, workspace: false, lease: false },
});
expect(diffEffectiveRunConfigFingerprints(
base,
createEffectiveRunConfigFingerprints({
session: { adapterType: "codex_local", model: "gpt-5.2" },
workspace: { workspaceStrategy: { type: "git_worktree", branchTemplate: "custom-{{issue.identifier}}" } },
lease: { environmentId: "environment-1", reuseLease: true },
}),
).changedCategories).toEqual(["workspace"]);
expect(diffEffectiveRunConfigFingerprints(
base,
createEffectiveRunConfigFingerprints({
session: { adapterType: "codex_local", model: "gpt-5.2" },
workspace: { workspaceStrategy: { type: "git_worktree", branchTemplate: "{{issue.identifier}}" } },
lease: { environmentId: "environment-2", reuseLease: true },
}),
).changedCategories).toEqual(["lease"]);
});
it("uses resolved secret version metadata without raw secret values", () => {
const v7 = createEffectiveRunConfigFingerprints({
session: {
adapterConfig: {
env: {
OPENAI_API_KEY: "resolved-secret-value",
PLAIN_TEXT: "plain env value",
},
},
},
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
version: 7,
provider: "local_encrypted",
providerVersionRef: "provider-version-7",
outcome: "success",
},
],
});
const canonical = v7.sessionFingerprint.canonicalJson;
expect(canonical).toContain("secret-1");
expect(canonical).toContain("binding-1");
expect(canonical).toContain("provider-version-7");
expect(canonical).toContain("\"version\":7");
expect(canonical).not.toContain("resolved-secret-value");
expect(canonical).not.toContain("plain env value");
expect(canonical).not.toContain("fingerprintSha256");
const rawValueChanged = createEffectiveRunConfigFingerprints({
session: {
adapterConfig: {
env: {
OPENAI_API_KEY: "different-resolved-secret-value",
PLAIN_TEXT: "plain env value",
},
},
},
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
version: 7,
provider: "local_encrypted",
providerVersionRef: "provider-version-7",
outcome: "success",
},
],
});
expect(rawValueChanged.sessionFingerprint.fingerprint).toBe(v7.sessionFingerprint.fingerprint);
const versionChanged = createEffectiveRunConfigFingerprints({
session: {
adapterConfig: {
env: {
OPENAI_API_KEY: "different-resolved-secret-value",
PLAIN_TEXT: "plain env value",
},
},
},
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
version: 8,
provider: "local_encrypted",
providerVersionRef: "provider-version-8",
outcome: "success",
},
],
});
expect(versionChanged.sessionFingerprint.fingerprint).not.toBe(v7.sessionFingerprint.fingerprint);
});
it("detects plain env value drift without storing raw values", () => {
const base = createEffectiveRunConfigFingerprints({
session: {
adapterConfig: {
env: {
FEATURE_FLAG: "enabled",
},
},
},
});
const changed = createEffectiveRunConfigFingerprints({
session: {
adapterConfig: {
env: {
FEATURE_FLAG: "disabled",
},
},
},
});
expect(changed.sessionFingerprint.fingerprint).not.toBe(base.sessionFingerprint.fingerprint);
expect(base.sessionFingerprint.canonicalJson).toContain('"valueHash":"sha256:');
expect(base.sessionFingerprint.canonicalJson).not.toContain("enabled");
expect(changed.sessionFingerprint.canonicalJson).not.toContain("disabled");
});
it("excludes generated run values, sensitive tokens, timestamps, and session path noise", () => {
const first = createEffectiveRunConfigFingerprints({
session: {
sessionId: "generated-session-one",
runId: "run-one",
cwd: "/runtime/noise/project-a",
updatedAt: "2026-06-01T00:00:00.000Z",
adapterConfig: {
token: "token-one",
nested: {
authorization: "Bearer first",
},
env: {
PAPERCLIP_API_KEY: "runtime-api-key-one",
NORMAL_VALUE: "first",
},
},
},
lease: {
leaseId: "lease-one",
providerLeaseId: "provider-lease-one",
remoteCwd: "/runtime/noise/remote-a",
createdAt: "2026-06-01T00:00:00.000Z",
driver: "daytona",
},
});
const second = createEffectiveRunConfigFingerprints({
session: {
sessionId: "generated-session-two",
runId: "run-two",
cwd: "/runtime/noise/project-b",
updatedAt: "2026-06-02T00:00:00.000Z",
adapterConfig: {
token: "token-two",
nested: {
authorization: "Bearer second",
},
env: {
PAPERCLIP_API_KEY: "runtime-api-key-two",
NORMAL_VALUE: "first",
},
},
},
lease: {
leaseId: "lease-two",
providerLeaseId: "provider-lease-two",
remoteCwd: "/runtime/noise/remote-b",
createdAt: "2026-06-02T00:00:00.000Z",
driver: "daytona",
},
});
expect(second.sessionFingerprint.fingerprint).toBe(first.sessionFingerprint.fingerprint);
expect(second.leaseFingerprint.fingerprint).toBe(first.leaseFingerprint.fingerprint);
const canonical = [
first.sessionFingerprint.canonicalJson,
first.leaseFingerprint.canonicalJson,
].join("\n");
expect(canonical).not.toContain("generated-session-one");
expect(canonical).not.toContain("run-one");
expect(canonical).not.toContain("/runtime/noise/project-a");
expect(canonical).not.toContain("runtime-api-key-one");
expect(canonical).not.toContain("token-one");
expect(canonical).not.toContain("Bearer first");
expect(canonical).not.toContain("lease-one");
expect(canonical).not.toContain("/runtime/noise/remote-a");
expect(canonicalizeEffectiveRunConfigCategory({
category: "workspace",
value: { cwd: "/explicit/runtime/workspace", workspaceStrategy: { type: "git_worktree" } },
})).toEqual({
cwd: "/explicit/runtime/workspace",
workspaceStrategy: { type: "git_worktree" },
});
});
});
@@ -392,7 +392,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
},
});
return { pluginId, companyId, executionWorkspaceId, reusableLease };
return { pluginId, companyId, agentId, environment, runId, executionWorkspaceId, reusableLease };
}
it("acquires and releases a local run lease through the runtime seam", async () => {
@@ -1217,6 +1217,413 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
});
});
it("does not resume released reusable plugin sandbox leases after provider config drift", async () => {
const pluginId = randomUUID();
const { companyId, agentId, environment: baseEnvironment, runId } = await seedEnvironment();
const providerConfig = {
provider: "fake-plugin",
image: "template-a",
timeoutMs: 1234,
reuseLease: true,
};
const environment = {
...baseEnvironment,
name: "Reusable Plugin Sandbox",
driver: "sandbox",
config: providerConfig,
};
await environmentService(db).update(environment.id, {
driver: "sandbox",
name: environment.name,
config: providerConfig,
});
await db.insert(plugins).values({
id: pluginId,
pluginKey: "acme.fake-sandbox-provider",
packageName: "@acme/fake-sandbox-provider",
version: "1.0.0",
apiVersion: 1,
categories: ["automation"],
manifestJson: {
id: "acme.fake-sandbox-provider",
apiVersion: 1,
version: "1.0.0",
displayName: "Fake Sandbox Provider",
description: "Test schema-driven provider",
author: "Paperclip",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: { worker: "dist/worker.js" },
environmentDrivers: [
{
driverKey: "fake-plugin",
kind: "sandbox_provider",
displayName: "Fake Plugin",
supportsReusableLeases: true,
configSchema: {
type: "object",
properties: {
image: { type: "string" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
],
},
status: "ready",
installOrder: 1,
updatedAt: new Date(),
} as any);
const executionWorkspaceId = randomUUID();
const projectId = randomUUID();
await db.insert(projects).values({
id: projectId,
companyId,
name: `Workspace ${projectId.slice(0, 8)}`,
status: "active",
createdAt: new Date(),
updatedAt: new Date(),
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
mode: "shared_workspace",
strategyType: "project_primary",
name: "Reusable workspace",
status: "active",
providerType: "local_fs",
createdAt: new Date(),
updatedAt: new Date(),
});
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
call: vi.fn(async (_pluginId: string, method: string, params: any) => {
if (method === "environmentAcquireLease") {
return {
providerLeaseId: `lease-${params.config.image}`,
metadata: {
provider: "fake-plugin",
image: params.config.image,
timeoutMs: params.config.timeoutMs,
reuseLease: true,
remoteCwd: "/workspace",
},
};
}
if (method === "environmentReleaseLease" || method === "environmentDestroyLease") {
return undefined;
}
throw new Error(`Unexpected plugin method: ${method}`);
}),
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const first = await runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: runId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
});
expect(first.lease.metadata?.reusableSandboxLease).toMatchObject({
provider: "fake-plugin",
leaseFingerprint: expect.objectContaining({
category: "lease",
fingerprint: expect.stringMatching(/^v1:sha256:[a-f0-9]{64}$/),
}),
});
await runtimeWithPlugin.releaseRunLeases(runId);
const nextRunId = randomUUID();
await db.insert(heartbeatRuns).values({
id: nextRunId,
companyId,
agentId,
invocationSource: "manual",
status: "running",
createdAt: new Date(),
updatedAt: new Date(),
});
const updatedEnvironment = {
...environment,
config: {
...providerConfig,
image: "template-b",
},
};
await environmentService(db).update(environment.id, {
config: updatedEnvironment.config,
});
const second = await runtimeWithPlugin.acquireRunLease({
companyId,
environment: updatedEnvironment,
issueId: null,
agentId,
heartbeatRunId: nextRunId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
});
expect(second.lease.providerLeaseId).toBe("lease-template-b");
expect(workerManager.call).not.toHaveBeenCalledWith(
pluginId,
"environmentResumeLease",
expect.anything(),
expect.anything(),
);
expect(workerManager.call).toHaveBeenCalledWith(pluginId, "environmentDestroyLease", expect.objectContaining({
providerLeaseId: "lease-template-a",
}), 31234);
await expect(environmentService(db).getLeaseById(first.lease.id)).resolves.toMatchObject({
status: "expired",
cleanupStatus: "success",
failureReason: "lease_fingerprint_mismatch",
});
});
it("does not resume released reusable plugin sandbox leases after secret version drift", async () => {
const pluginId = randomUUID();
const { companyId, agentId, environment: baseEnvironment, runId } = await seedEnvironment();
const apiSecret = await secretService(db).create(companyId, {
name: `secure-plugin-api-key-${randomUUID()}`,
provider: "local_encrypted",
value: "resolved-provider-key",
});
const providerConfig = {
provider: "secure-plugin",
template: "base",
apiKey: apiSecret.id,
timeoutMs: 1234,
reuseLease: true,
};
const environment = {
...baseEnvironment,
name: "Secure Plugin Sandbox",
driver: "sandbox",
config: providerConfig,
};
await secretService(db).createBinding({
companyId,
secretId: apiSecret.id,
targetType: "environment",
targetId: environment.id,
configPath: "apiKey",
});
await environmentService(db).update(environment.id, {
driver: "sandbox",
name: environment.name,
config: providerConfig,
});
await db.insert(plugins).values({
id: pluginId,
pluginKey: "acme.secure-sandbox-provider",
packageName: "@acme/secure-sandbox-provider",
version: "1.0.0",
apiVersion: 1,
categories: ["automation"],
manifestJson: {
id: "acme.secure-sandbox-provider",
apiVersion: 1,
version: "1.0.0",
displayName: "Secure Sandbox Provider",
description: "Test schema-driven provider",
author: "Paperclip",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: { worker: "dist/worker.js" },
environmentDrivers: [
{
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
supportsReusableLeases: true,
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
],
},
status: "ready",
installOrder: 1,
updatedAt: new Date(),
} as any);
const executionWorkspaceId = randomUUID();
const projectId = randomUUID();
await db.insert(projects).values({
id: projectId,
companyId,
name: `Workspace ${projectId.slice(0, 8)}`,
status: "active",
createdAt: new Date(),
updatedAt: new Date(),
});
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
mode: "shared_workspace",
strategyType: "project_primary",
name: "Reusable workspace",
status: "active",
providerType: "local_fs",
createdAt: new Date(),
updatedAt: new Date(),
});
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
call: vi.fn(async (_pluginId: string, method: string, params: any) => {
if (method === "environmentAcquireLease") {
return {
providerLeaseId: `lease-${params.config.apiKey}`,
metadata: {
provider: "secure-plugin",
template: params.config.template,
apiKey: params.config.apiKey,
timeoutMs: params.config.timeoutMs,
reuseLease: true,
remoteCwd: "/workspace",
},
};
}
if (method === "environmentReleaseLease" || method === "environmentDestroyLease") {
return undefined;
}
throw new Error(`Unexpected plugin method: ${method}`);
}),
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const first = await runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: runId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
});
await runtimeWithPlugin.releaseRunLeases(runId);
await secretService(db).rotate(apiSecret.id, { value: "rotated-provider-key" });
const nextRunId = randomUUID();
await db.insert(heartbeatRuns).values({
id: nextRunId,
companyId,
agentId,
invocationSource: "manual",
status: "running",
createdAt: new Date(),
updatedAt: new Date(),
});
const second = await runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: nextRunId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
});
expect(second.lease.providerLeaseId).toBe("lease-rotated-provider-key");
expect(workerManager.call).not.toHaveBeenCalledWith(
pluginId,
"environmentResumeLease",
expect.anything(),
expect.anything(),
);
expect(workerManager.call).toHaveBeenCalledWith(pluginId, "environmentDestroyLease", expect.objectContaining({
providerLeaseId: "lease-resolved-provider-key",
}), 31234);
await expect(environmentService(db).getLeaseById(first.lease.id)).resolves.toMatchObject({
status: "expired",
cleanupStatus: "success",
failureReason: "lease_fingerprint_mismatch",
});
const firstMetadata = JSON.stringify(first.lease.metadata);
expect(firstMetadata).not.toContain("resolved-provider-key");
expect(firstMetadata).not.toContain("rotated-provider-key");
});
it("preserves active reusable sandbox leases held by another running run", async () => {
const { pluginId, companyId, agentId, environment, executionWorkspaceId, reusableLease } =
await seedReusablePluginSandboxLease();
const nextRunId = randomUUID();
await db.insert(heartbeatRuns).values({
id: nextRunId,
companyId,
agentId,
invocationSource: "manual",
status: "running",
createdAt: new Date(),
updatedAt: new Date(),
});
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
call: vi.fn(async (_pluginId: string, method: string) => {
if (method === "environmentAcquireLease") {
return {
providerLeaseId: "fresh-plugin-lease",
metadata: {
provider: "fake-plugin",
image: "fake:test",
timeoutMs: 1234,
reuseLease: true,
remoteCwd: "/workspace",
},
};
}
throw new Error(`Unexpected plugin method: ${method}`);
}),
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const acquired = await runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: nextRunId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
});
expect(acquired.lease.providerLeaseId).toBe("fresh-plugin-lease");
expect(workerManager.call).toHaveBeenCalledOnce();
expect(workerManager.call).toHaveBeenCalledWith(pluginId, "environmentAcquireLease", expect.objectContaining({
agentId,
executionWorkspaceId,
runId: nextRunId,
}), 31234);
await expect(environmentService(db).getLeaseById(reusableLease.id)).resolves.toMatchObject({
status: "active",
cleanupStatus: null,
});
});
it("does not retain or resume plugin-backed sandbox leases unless the provider opts in", async () => {
const pluginId = randomUUID();
const { companyId, agentId, environment: baseEnvironment, runId } = await seedEnvironment();
@@ -11,8 +11,10 @@ import {
applyPersistedExecutionWorkspaceConfig,
assertGitSensitiveAdapterWorkspaceValid,
assertPushCapabilityCheckoutValid,
buildRealizedExecutionWorkspaceFromPersisted,
buildExplicitResumeSessionOverride,
buildEffectiveRunSessionConfigMetadata,
buildEffectiveRunWorkspaceConfigMetadata,
buildWorkspaceConfigFreshnessOperation,
deriveTaskKeyWithHeartbeatFallback,
extractWakeCommentIds,
formatRuntimeWorkspaceWarningLog,
@@ -21,7 +23,9 @@ import {
preflightLowTrustWorkspaceIsolation,
prioritizeProjectWorkspaceCandidatesForRun,
parseSessionCompactionPolicy,
resolveExecutionWorkspaceConfigFreshness,
resolveNextSessionState,
resolveTaskSessionConfigFreshness,
requiresPushCapabilityPreflight,
resolveWorkspaceAfterLowTrustPreflight,
resolveRuntimeSessionParamsForWorkspace,
@@ -30,6 +34,7 @@ import {
stripWorkspaceRuntimeFromExecutionRunConfig,
shouldResetTaskSessionForModelChange,
stripConfiguredModelFromSessionParams,
stripPaperclipSessionMetadataFromSessionParams,
normalizeSessionParams,
shouldResetTaskSessionForWake,
type ResolvedWorkspaceForRun,
@@ -812,48 +817,266 @@ describe("mergeExecutionWorkspaceMetadataForPersistence", () => {
});
});
describe("buildRealizedExecutionWorkspaceFromPersisted", () => {
it("reuses the persisted execution workspace path instead of deriving a new worktree", () => {
const result = buildRealizedExecutionWorkspaceFromPersisted({
base: buildResolvedWorkspace({
cwd: "/tmp/project-primary",
repoRef: "main",
}),
workspace: {
id: "execution-workspace-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
sourceIssueId: "issue-1",
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "PAP-880-thumbs-capture-for-evals-feature",
status: "active",
cwd: "/tmp/reused-worktree",
repoUrl: "https://example.com/paperclip.git",
baseRef: "main",
branchName: "PAP-880-thumbs-capture-for-evals-feature",
providerType: "git_worktree",
providerRef: "/tmp/reused-worktree",
derivedFromExecutionWorkspaceId: null,
lastUsedAt: new Date(),
openedAt: new Date(),
closedAt: null,
cleanupEligibleAt: null,
cleanupReason: null,
config: null,
metadata: null,
createdAt: new Date(),
updatedAt: new Date(),
type WorkspaceConfigMetadata = ReturnType<typeof buildEffectiveRunWorkspaceConfigMetadata>;
function buildWorkspaceConfigMetadata(
overrides: Partial<Parameters<typeof buildEffectiveRunWorkspaceConfigMetadata>[0]> = {},
) {
return buildEffectiveRunWorkspaceConfigMetadata({
mode: "isolated_workspace",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
strategyType: "git_worktree",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/main",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
repoUrl: "https://github.com/example/repo.git",
repoRef: "origin/main",
configSnapshot: {
provisionCommand: "pnpm install",
teardownCommand: "pnpm stop",
cleanupCommand: "pnpm clean",
desiredState: "running",
serviceStates: { "0": "running" },
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev", port: 3100 }],
},
},
environment: {
selectedEnvironmentId: "environment-1",
driver: "local",
config: { provider: "local" },
},
realization: {
environmentDriver: "local",
environmentProvider: "local",
},
evaluatedAt: "2026-06-26T00:00:00.000Z",
...overrides,
});
}
function persistedWorkspaceConfigFingerprint(metadata: WorkspaceConfigMetadata) {
return {
configFingerprint: {
version: metadata.version,
workspaceHash: metadata.fingerprint,
categories: metadata.categories,
categoryFingerprints: metadata.categoryFingerprints,
lastEvaluatedAt: metadata.evaluatedAt,
},
};
}
describe("effective run execution workspace config freshness", () => {
it("reuses an existing workspace when the stored workspace fingerprint is unchanged", () => {
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(metadata),
nextMetadata: metadata,
});
expect(decision).toMatchObject({
action: "reuse",
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: false,
changedCategories: [],
storedFingerprintPresent: true,
});
});
it("refreshes metadata and config for runtime-service-only drift without replacing the workspace", () => {
const base = buildWorkspaceConfigMetadata();
const next = buildWorkspaceConfigMetadata({
configSnapshot: {
provisionCommand: "pnpm install",
teardownCommand: "pnpm stop",
cleanupCommand: "pnpm clean",
desiredState: "running",
serviceStates: { "0": "running" },
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
},
});
expect(result.created).toBe(false);
expect(result.strategy).toBe("git_worktree");
expect(result.cwd).toBe("/tmp/reused-worktree");
expect(result.worktreePath).toBe("/tmp/reused-worktree");
expect(result.branchName).toBe("PAP-880-thumbs-capture-for-evals-feature");
expect(result.source).toBe("task_session");
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
expect(decision).toMatchObject({
action: "refresh",
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: true,
changedCategories: ["runtimeServices"],
});
const metadata = mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: {
config: {
workspaceRuntime: { services: [{ name: "web", command: "pnpm dev", port: 3100 }] },
},
...persistedWorkspaceConfigFingerprint(base),
},
source: "task_session",
createdByRuntime: false,
configSnapshot: {
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
desiredState: "running",
serviceStates: { "0": "running" },
},
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: true,
workspaceConfigMetadata: next,
baseRef: "origin/main",
baseRefSha: "abc123",
});
expect(metadata?.config).toMatchObject({
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
});
expect(metadata?.configFingerprint).toMatchObject({
workspaceHash: next.fingerprint,
categories: next.categories,
});
});
it.each([
{
name: "mode",
category: "mode",
next: buildWorkspaceConfigMetadata({ mode: "shared_workspace" }),
},
{
name: "workspace strategy",
category: "strategy",
next: buildWorkspaceConfigMetadata({
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/main",
branchTemplate: "custom-{{issue.identifier}}",
worktreeParentDir: ".paperclip/worktrees",
},
}),
},
{
name: "project workspace",
category: "projectWorkspace",
next: buildWorkspaceConfigMetadata({ projectWorkspaceId: "workspace-2" }),
},
{
name: "base ref",
category: "repo",
next: buildWorkspaceConfigMetadata({
repoRef: "origin/release",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/release",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
}),
},
{
name: "environment realization",
category: "environment",
next: buildWorkspaceConfigMetadata({
environment: {
selectedEnvironmentId: "environment-2",
driver: "sandbox",
config: { provider: "daytona" },
},
realization: {
environmentDriver: "sandbox",
environmentProvider: "daytona",
},
}),
},
] as const)("replaces the workspace when $name changes", ({ category, next }) => {
const base = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
expect(decision.action).toBe("replace");
expect(decision.shouldReuseExisting).toBe(false);
expect(decision.changedCategories).toContain(category);
});
it("formats a safe workspace operation payload for config drift decisions", () => {
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(buildWorkspaceConfigMetadata()),
nextMetadata: buildWorkspaceConfigMetadata({
configSnapshot: {
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
},
}),
});
const operation = buildWorkspaceConfigFreshnessOperation({
decision,
hasExistingWorkspace: true,
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: true,
previousWorkspaceId: "workspace-old",
activeWorkspaceId: "workspace-old",
});
expect(operation).toMatchObject({
metadata: {
kind: "config_freshness",
action: "refresh",
changedCategories: ["lifecycleCommands", "runtimeServices"],
changedCategoryLabels: ["workspace lifecycle commands", "runtime services"],
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: true,
previousWorkspaceId: "workspace-old",
activeWorkspaceId: "workspace-old",
},
system: expect.stringContaining("refreshed execution workspace config"),
});
const serialized = JSON.stringify(operation);
expect(serialized).toContain("runtime services");
expect(serialized).not.toContain("pnpm dev");
expect(serialized).not.toContain("0.0.0.0");
});
it("does not record a freshness operation when an unchanged workspace is simply reused", () => {
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(metadata),
nextMetadata: metadata,
});
expect(buildWorkspaceConfigFreshnessOperation({
decision,
hasExistingWorkspace: true,
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: false,
previousWorkspaceId: "workspace-1",
activeWorkspaceId: "workspace-1",
})).toBeNull();
});
});
@@ -1075,6 +1298,332 @@ describe("shouldResetTaskSessionForModelChange", () => {
});
});
type SessionConfigMetadata = Awaited<ReturnType<typeof buildEffectiveRunSessionConfigMetadata>>;
async function buildSessionConfigMetadata(
overrides: Partial<Parameters<typeof buildEffectiveRunSessionConfigMetadata>[0]> = {},
) {
return buildEffectiveRunSessionConfigMetadata({
adapterType: "codex_local",
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
env: {
OPENAI_API_KEY: "resolved-secret-value",
PLAIN_FLAG: "plain-value",
},
},
agentRuntimeConfig: {
heartbeat: {
maxConcurrentRuns: 1,
},
},
modelProfile: null,
issueOverrides: null,
workspaceConfig: {
requestedMode: "agent_default",
effectiveMode: "agent_default",
projectConfigRevisionAt: "2026-06-01T00:00:00.000Z",
},
environment: {
selectionSource: "default",
selectedEnvironmentId: "environment-1",
selectedEnvironment: {
id: "environment-1",
driver: "local",
configRevisionAt: "2026-06-01T00:00:00.000Z",
},
},
environmentEnv: {
ENVIRONMENT_FLAG: "enabled",
},
projectEnv: {
PROJECT_FLAG: "enabled",
},
routineEnv: null,
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
secretKey: "openai-api-key",
version: 7,
provider: "local_encrypted",
outcome: "success",
},
],
runtimeSkills: [
{
key: "paperclip",
runtimeName: "paperclip",
source: "/tmp/paperclip/runtime-skills/paperclip",
versionId: null,
currentVersionId: "skill-version-1",
sourceStatus: "available",
missingDetail: null,
},
],
agentConfigRevision: {
id: "agent-config-revision-1",
changedKeys: ["adapterConfig"],
configRevisionAt: "2026-06-01T00:00:00.000Z",
},
...overrides,
});
}
function sessionParamsWithConfigMetadata(
metadata: SessionConfigMetadata,
configuredModel = "gpt-5.4-mini",
) {
return {
sessionId: "thread-1",
__paperclipConfiguredModel: configuredModel,
__paperclipConfigFingerprint: metadata.fingerprint,
__paperclipConfigFingerprintVersion: metadata.version,
__paperclipConfigCategories: metadata.categories,
__paperclipConfigCategoryFingerprints: metadata.categoryFingerprints,
};
}
describe("effective run session config freshness", () => {
it("resets when effective adapter config changes after model/profile/env resolution", async () => {
const base = await buildSessionConfigMetadata();
const next = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
approvalPolicy: "never",
},
});
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: next,
});
expect(decision).toMatchObject({
reset: true,
changedCategories: ["adapterConfig"],
});
expect(decision.reasons.join("\n")).toContain("adapter config");
});
it("keeps model-only compatibility as an additional reset reason", async () => {
const base = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base, "opencode/mimo-v2-pro-free"),
configMetadata: base,
});
expect(decision.reset).toBe(true);
expect(decision.reasons).toEqual([
'configured model changed from "opencode/mimo-v2-pro-free" to "gpt-5.4-mini"',
]);
});
it("freshens legacy task sessions that lack versioned config metadata", async () => {
const metadata = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
configMetadata: metadata,
});
expect(decision.reset).toBe(true);
expect(decision.changedCategories).toEqual(metadata.categories);
expect(decision.reasons).toEqual(["effective run configuration fingerprint metadata is missing"]);
});
it("preserves legacy metadata gaps only for active accepted-plan continuation sessions", async () => {
const metadata = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
configMetadata: metadata,
preserveLegacySessionWithoutConfigMetadata: true,
});
expect(decision.reset).toBe(false);
expect(decision.changedCategories).toEqual([]);
expect(decision.reasons).toEqual([]);
});
it("names safe categories for model profile, issue override, env, secret, and runtime skill drift", async () => {
const base = await buildSessionConfigMetadata();
const cases: Array<{
name: string;
category: string;
metadata: SessionConfigMetadata;
}> = [
{
name: "model profile",
category: "modelProfile",
metadata: await buildSessionConfigMetadata({
modelProfile: {
requested: "cheap",
applied: true,
configSource: "agent_runtime",
},
}),
},
{
name: "issue overrides",
category: "issueOverrides",
metadata: await buildSessionConfigMetadata({
issueOverrides: {
adapterConfig: {
reasoningEffort: "high",
},
},
}),
},
{
name: "project env bindings",
category: "envBindings",
metadata: await buildSessionConfigMetadata({
projectEnv: {
PROJECT_FLAG: "enabled",
NEW_PROJECT_FLAG: "present",
},
}),
},
{
name: "secret version",
category: "secrets",
metadata: await buildSessionConfigMetadata({
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
secretKey: "openai-api-key",
version: 8,
provider: "local_encrypted",
outcome: "success",
},
],
}),
},
{
name: "runtime skills",
category: "runtimeSkills",
metadata: await buildSessionConfigMetadata({
runtimeSkills: [
{
key: "paperclip",
runtimeName: "paperclip",
source: "/tmp/paperclip/runtime-skills/paperclip",
versionId: null,
currentVersionId: "skill-version-2",
sourceStatus: "available",
missingDetail: null,
},
],
}),
},
];
for (const testCase of cases) {
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: testCase.metadata,
});
expect(decision.reset, testCase.name).toBe(true);
expect(decision.changedCategories, testCase.name).toContain(testCase.category);
}
});
it("detects instructions content drift without storing the contents", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-session-fingerprint-"));
const instructionsPath = path.join(root, "AGENTS.md");
await fs.writeFile(instructionsPath, "Version one instructions.\n", "utf8");
const base = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsBundleMode: "managed",
instructionsRootPath: root,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: instructionsPath,
},
});
await fs.writeFile(instructionsPath, "Version two instructions.\n", "utf8");
const next = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsBundleMode: "managed",
instructionsRootPath: root,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: instructionsPath,
},
});
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: next,
});
expect(decision.reset).toBe(true);
expect(decision.changedCategories).toContain("instructions");
expect(next.fingerprints.sessionFingerprint.canonicalJson).not.toContain("Version two instructions");
});
it("does not read unbounded legacy instructions paths for config fingerprints", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-session-fingerprint-"));
const instructionsPath = path.join(root, "AGENTS.md");
await fs.writeFile(instructionsPath, "Legacy direct-path instructions.\n", "utf8");
const metadata = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsFilePath: instructionsPath,
},
});
const canonical = metadata.fingerprints.sessionFingerprint.canonicalJson;
expect(canonical).toContain("missing_absolute_root");
expect(canonical).not.toContain("Legacy direct-path instructions");
expect(canonical).not.toContain("contentHash");
});
it("does not include raw secret or plain env values in canonical session metadata", async () => {
const metadata = await buildSessionConfigMetadata();
const canonical = metadata.fingerprints.sessionFingerprint.canonicalJson;
expect(canonical).toContain("secret-1");
expect(canonical).toContain('"version":7');
expect(canonical).not.toContain("resolved-secret-value");
expect(canonical).not.toContain("plain-value");
expect(canonical).not.toContain("enabled");
expect(canonical).not.toContain("openai-api-key");
});
});
describe("stripConfiguredModelFromSessionParams", () => {
it("removes the internal model key from persisted session params", () => {
expect(
@@ -1108,6 +1657,25 @@ describe("stripConfiguredModelFromSessionParams", () => {
});
});
describe("stripPaperclipSessionMetadataFromSessionParams", () => {
it("removes all internal Paperclip session metadata before adapter invocation", () => {
expect(
stripPaperclipSessionMetadataFromSessionParams({
sessionId: "thread-1",
cwd: "/tmp/project",
__paperclipConfiguredModel: "gpt-5.4-mini",
__paperclipConfigFingerprint: "v1:sha256:abc",
__paperclipConfigFingerprintVersion: 1,
__paperclipConfigCategories: ["adapterConfig"],
__paperclipConfigCategoryFingerprints: { adapterConfig: "v1:sha256:def" },
}),
).toEqual({
sessionId: "thread-1",
cwd: "/tmp/project",
});
});
});
describe("normalizeSessionParams", () => {
it("collapses an empty object to null", () => {
expect(normalizeSessionParams({})).toBeNull();
@@ -2057,6 +2057,45 @@ describe("realizeExecutionWorkspace", () => {
expect(actualHead).toBe(expectedHead);
}, 15_000);
it("does not reuse a missing persisted local filesystem workspace", async () => {
const baseCwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-workspace-base-"));
const missingCwd = path.join(baseCwd, "missing-workspace");
const restored = await ensurePersistedExecutionWorkspaceAvailable({
base: {
baseCwd,
source: "project_primary",
projectId: "project-1",
workspaceId: null,
repoUrl: null,
repoRef: null,
},
workspace: {
mode: "shared_workspace",
strategyType: "project_primary",
cwd: missingCwd,
providerRef: null,
projectId: "project-1",
projectWorkspaceId: null,
repoUrl: null,
baseRef: null,
branchName: null,
},
issue: {
id: "issue-1",
identifier: "PAP-453",
title: "Missing local workspace",
},
agent: {
id: "agent-1",
name: "Codex Coder",
companyId: "company-1",
},
});
expect(restored).toBeNull();
});
it("reprovisions an existing persisted git worktree before manual control starts it", async () => {
const repoRoot = await createTempRepo();
await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true });
@@ -2195,6 +2234,7 @@ describe("realizeExecutionWorkspace", () => {
it("auto-detects the default branch via symbolic-ref when origin/HEAD is set", async () => {
const repoRoot = await createTempRepo("main");
await runGit(repoRoot, ["branch", "-f", "master", "main"]);
const bareRemote = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-bare-symref-"));
await runGit(bareRemote, ["init", "--bare"]);
@@ -0,0 +1,492 @@
import { createHash } from "node:crypto";
import type { RuntimeSecretManifestEntry } from "./secrets.js";
export const EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION = 1;
export const EFFECTIVE_RUN_CONFIG_FINGERPRINT_ALGORITHM = "sha256";
export const EFFECTIVE_RUN_CONFIG_FINGERPRINT_CATEGORIES = ["session", "workspace", "lease"] as const;
export type EffectiveRunConfigFingerprintCategory =
(typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_CATEGORIES)[number];
export type EffectiveRunConfigChangedCategory = EffectiveRunConfigFingerprintCategory;
export type EffectiveRunConfigCanonicalValue =
| null
| boolean
| number
| string
| EffectiveRunConfigCanonicalValue[]
| { [key: string]: EffectiveRunConfigCanonicalValue };
export interface EffectiveRunConfigSecretVersionMetadata {
configPath: string;
envKey: string | null;
secretId: string;
bindingId?: string | null;
version: number | string;
provider?: string | null;
providerVersionRef?: string | null;
outcome?: "success" | "failure" | null;
}
export type EffectiveRunConfigSecretManifestEntry =
| RuntimeSecretManifestEntry
| EffectiveRunConfigSecretVersionMetadata;
export interface EffectiveRunConfigFingerprintInput {
session?: unknown;
workspace?: unknown;
lease?: unknown;
secretManifest?: readonly EffectiveRunConfigSecretManifestEntry[];
}
export interface EffectiveRunConfigFingerprint {
version: typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION;
category: EffectiveRunConfigFingerprintCategory;
algorithm: typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_ALGORITHM;
fingerprint: string;
canonicalJson: string;
}
export interface EffectiveRunConfigFingerprints {
version: typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION;
categories: readonly EffectiveRunConfigFingerprintCategory[];
sessionFingerprint: EffectiveRunConfigFingerprint;
workspaceFingerprint: EffectiveRunConfigFingerprint;
leaseFingerprint: EffectiveRunConfigFingerprint;
}
export interface EffectiveRunConfigFingerprintDiff {
version: typeof EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION;
hasChanges: boolean;
changedCategories: EffectiveRunConfigChangedCategory[];
changed: Record<EffectiveRunConfigChangedCategory, boolean>;
}
const OMIT = Symbol("omit-from-effective-run-config-fingerprint");
const REDACTED_VALUE: EffectiveRunConfigCanonicalValue = { type: "redacted", present: true };
const GENERATED_RUNTIME_ENV_KEY_RE = /^PAPERCLIP_/;
const SENSITIVE_CONFIG_KEY_RE =
/(?:api[_-]?key|access[_-]?token|auth(?:orization)?|bearer|cookie|credential|jwt|password|passwd|private[_-]?key|secret|token)$/i;
const VOLATILE_CONFIG_KEYS = new Set([
"checkoutRunId",
"executionRunId",
"externalRunId",
"heartbeatRunId",
"invocationId",
"leaseId",
"providerLeaseId",
"requestId",
"runId",
"sessionDisplayId",
"sessionId",
"spanId",
"traceId",
]);
const HOST_NOISE_KEYS = new Set([
"agentHome",
"homeDir",
"hostCwd",
"localHome",
"tempDir",
"tmpDir",
"userHome",
]);
const SESSION_HOST_PATH_KEYS = new Set([
"cwd",
"localPath",
"remoteCwd",
"workspaceCwd",
"workspacePath",
"workspaceRemoteDir",
"worktreePath",
]);
type SecretManifestIndex = {
byConfigPath: Map<string, EffectiveRunConfigSecretVersionMetadata>;
byEnvKey: Map<string, EffectiveRunConfigSecretVersionMetadata>;
};
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value)
&& typeof value === "object"
&& !Array.isArray(value)
&& !(value instanceof Date);
}
function readString(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function readVersion(value: unknown) {
if (typeof value === "number" && Number.isFinite(value)) return value;
return readString(value);
}
function normalizeSecretManifestEntry(
entry: EffectiveRunConfigSecretManifestEntry,
): EffectiveRunConfigSecretVersionMetadata | null {
const record = entry as Record<string, unknown>;
const secretId = readString(record.secretId);
const version = readVersion(record.version);
if (!secretId || version === null) return null;
const normalized: EffectiveRunConfigSecretVersionMetadata = {
configPath: readString(record.configPath) ?? "",
envKey: readString(record.envKey),
secretId,
version,
};
const bindingId = readString(record.bindingId);
const provider = readString(record.provider);
const providerVersionRef = readString(record.providerVersionRef);
const outcome = record.outcome === "success" || record.outcome === "failure"
? record.outcome
: null;
if (bindingId !== null) normalized.bindingId = bindingId;
if (provider !== null) normalized.provider = provider;
if (providerVersionRef !== null) normalized.providerVersionRef = providerVersionRef;
if (outcome !== null) normalized.outcome = outcome;
return normalized;
}
function buildSecretManifestIndex(
manifest: readonly EffectiveRunConfigSecretManifestEntry[] | undefined,
): SecretManifestIndex {
const byConfigPath = new Map<string, EffectiveRunConfigSecretVersionMetadata>();
const byEnvKey = new Map<string, EffectiveRunConfigSecretVersionMetadata>();
for (const entry of manifest ?? []) {
const normalized = normalizeSecretManifestEntry(entry);
if (!normalized) continue;
if (normalized.configPath) byConfigPath.set(normalized.configPath, normalized);
if (normalized.envKey) byEnvKey.set(normalized.envKey, normalized);
}
return { byConfigPath, byEnvKey };
}
function canonicalSecretMetadata(
metadata: EffectiveRunConfigSecretVersionMetadata,
): EffectiveRunConfigCanonicalValue {
return omitNullish({
type: "secret_ref",
configPath: metadata.configPath || undefined,
envKey: metadata.envKey ?? undefined,
secretId: metadata.secretId,
bindingId: metadata.bindingId ?? undefined,
version: metadata.version,
provider: metadata.provider ?? undefined,
providerVersionRef: metadata.providerVersionRef ?? undefined,
outcome: metadata.outcome ?? undefined,
});
}
function isSecretRefBinding(value: unknown): value is Record<string, unknown> {
return isPlainObject(value)
&& value.type === "secret_ref"
&& typeof value.secretId === "string"
&& value.secretId.trim().length > 0;
}
function canonicalSecretRefBinding(
value: Record<string, unknown>,
configPath: string,
): EffectiveRunConfigCanonicalValue {
return omitNullish({
type: "secret_ref",
configPath,
secretId: readString(value.secretId),
versionSelector: readVersion(value.version) ?? "latest",
unresolved: true,
});
}
function omitNullish(record: Record<string, unknown>): EffectiveRunConfigCanonicalValue {
return Object.fromEntries(
Object.entries(record).filter(([, value]) => value !== undefined && value !== null),
) as EffectiveRunConfigCanonicalValue;
}
function isTimestampNoiseKey(key: string) {
return /(created|updated|started|finished|completed|cancelled|resolved|used|heartbeat)At$/i.test(key)
&& !/(revision|version)/i.test(key);
}
function shouldOmitObjectKey(
category: EffectiveRunConfigFingerprintCategory,
key: string,
) {
if (VOLATILE_CONFIG_KEYS.has(key)) return true;
if (HOST_NOISE_KEYS.has(key)) return true;
if (isTimestampNoiseKey(key)) return true;
if (category === "session" && SESSION_HOST_PATH_KEYS.has(key)) return true;
if (category === "lease" && (key === "remoteCwd" || key === "workspaceRemoteDir")) return true;
return false;
}
function stableStringify(value: EffectiveRunConfigCanonicalValue): string {
if (Array.isArray(value)) {
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
}
if (value && typeof value === "object") {
const record = value as Record<string, EffectiveRunConfigCanonicalValue>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key] ?? null)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function canonicalizePlainEnvValueForHash(value: unknown): EffectiveRunConfigCanonicalValue {
if (value === undefined || value === null) return null;
if (Array.isArray(value)) {
return value.map((entry) => canonicalizePlainEnvValueForHash(entry));
}
if (isPlainObject(value)) {
const out: Record<string, EffectiveRunConfigCanonicalValue> = {};
for (const key of Object.keys(value).sort()) {
const next = canonicalizePlainEnvValueForHash(value[key]);
if (next !== null) out[key] = next;
}
return out;
}
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}
if (typeof value === "bigint") {
return value.toString();
}
if (typeof value === "boolean" || typeof value === "string") {
return value;
}
return String(value);
}
function hashPlainEnvValue(value: unknown): string | null {
if (value === undefined || value === null) return null;
const canonicalJson = stableStringify(canonicalizePlainEnvValueForHash(value));
return `sha256:${createHash("sha256").update(canonicalJson).digest("hex")}`;
}
function canonicalizeEnvRecord(
envValue: unknown,
context: CanonicalizeContext,
): EffectiveRunConfigCanonicalValue | typeof OMIT {
if (!isPlainObject(envValue)) return {};
const canonicalEnv: Record<string, EffectiveRunConfigCanonicalValue> = {};
for (const key of Object.keys(envValue).sort()) {
if (GENERATED_RUNTIME_ENV_KEY_RE.test(key)) continue;
const manifestEntry = context.secrets.byConfigPath.get(`env.${key}`) ?? context.secrets.byEnvKey.get(key);
if (manifestEntry) {
canonicalEnv[key] = canonicalSecretMetadata(manifestEntry);
continue;
}
const rawBinding = envValue[key];
if (isSecretRefBinding(rawBinding)) {
canonicalEnv[key] = canonicalSecretRefBinding(rawBinding, `env.${key}`);
continue;
}
canonicalEnv[key] = omitNullish({
type: "plain_env",
present: rawBinding !== undefined && rawBinding !== null,
valueHash: hashPlainEnvValue(rawBinding),
});
}
return Object.keys(canonicalEnv).length > 0 ? canonicalEnv : OMIT;
}
type CanonicalizeContext = {
category: EffectiveRunConfigFingerprintCategory;
path: string[];
secrets: SecretManifestIndex;
};
function canonicalizeValue(
value: unknown,
context: CanonicalizeContext,
): EffectiveRunConfigCanonicalValue | typeof OMIT {
if (value === undefined || value instanceof Date) return OMIT;
if (value === null) return null;
if (Array.isArray(value)) {
return value.map((entry, index) => {
const next = canonicalizeValue(entry, { ...context, path: [...context.path, String(index)] });
return next === OMIT ? null : next;
});
}
if (isSecretRefBinding(value)) {
return canonicalSecretRefBinding(value, context.path.join("."));
}
if (isPlainObject(value)) {
const canonicalObject: Record<string, EffectiveRunConfigCanonicalValue> = {};
for (const key of Object.keys(value).sort()) {
if (key === "env") {
const env = canonicalizeEnvRecord(value[key], { ...context, path: [...context.path, key] });
if (env !== OMIT) canonicalObject[key] = env;
continue;
}
if (shouldOmitObjectKey(context.category, key)) continue;
if (SENSITIVE_CONFIG_KEY_RE.test(key)) {
canonicalObject[key] = REDACTED_VALUE;
continue;
}
const next = canonicalizeValue(value[key], { ...context, path: [...context.path, key] });
if (next !== OMIT) canonicalObject[key] = next;
}
return Object.keys(canonicalObject).length > 0 ? canonicalObject : {};
}
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}
if (typeof value === "bigint") {
return value.toString();
}
if (typeof value === "boolean" || typeof value === "string") {
return value;
}
return null;
}
export function canonicalizeEffectiveRunConfigCategory(input: {
category: EffectiveRunConfigFingerprintCategory;
value: unknown;
secretManifest?: readonly EffectiveRunConfigSecretManifestEntry[];
}): EffectiveRunConfigCanonicalValue {
const canonical = canonicalizeValue(input.value ?? {}, {
category: input.category,
path: [],
secrets: buildSecretManifestIndex(input.secretManifest),
});
return canonical === OMIT ? {} : canonical;
}
function createCategoryFingerprint(input: {
category: EffectiveRunConfigFingerprintCategory;
value: unknown;
secretManifest?: readonly EffectiveRunConfigSecretManifestEntry[];
}): EffectiveRunConfigFingerprint {
const canonicalValue = canonicalizeEffectiveRunConfigCategory(input);
const canonicalJson = stableStringify({
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
category: input.category,
value: canonicalValue,
});
const digest = createHash("sha256").update(canonicalJson).digest("hex");
return {
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
category: input.category,
algorithm: EFFECTIVE_RUN_CONFIG_FINGERPRINT_ALGORITHM,
fingerprint: `v${EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION}:sha256:${digest}`,
canonicalJson,
};
}
function createCategoryFingerprintFromCanonicalValue(input: {
category: EffectiveRunConfigFingerprintCategory;
value: EffectiveRunConfigCanonicalValue;
}): EffectiveRunConfigFingerprint {
const canonicalJson = stableStringify({
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
category: input.category,
value: input.value,
});
const digest = createHash("sha256").update(canonicalJson).digest("hex");
return {
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
category: input.category,
algorithm: EFFECTIVE_RUN_CONFIG_FINGERPRINT_ALGORITHM,
fingerprint: `v${EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION}:sha256:${digest}`,
canonicalJson,
};
}
function canonicalRecord(value: EffectiveRunConfigCanonicalValue) {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, EffectiveRunConfigCanonicalValue>
: {};
}
export function createEffectiveRunConfigSubcategoryFingerprints<T extends string>(input: {
category: EffectiveRunConfigFingerprintCategory;
value: Record<T, unknown>;
subcategories: readonly T[];
secretManifest?: readonly EffectiveRunConfigSecretManifestEntry[];
}): Record<T, string> {
const canonicalValue = canonicalizeEffectiveRunConfigCategory({
category: input.category,
value: input.value,
secretManifest: input.secretManifest,
});
const record = canonicalRecord(canonicalValue);
return Object.fromEntries(
input.subcategories.map((subcategory) => {
const value = Object.prototype.hasOwnProperty.call(record, subcategory)
? { [subcategory]: record[subcategory] ?? null }
: {};
return [
subcategory,
createCategoryFingerprintFromCanonicalValue({
category: input.category,
value,
}).fingerprint,
];
}),
) as Record<T, string>;
}
export function createEffectiveRunConfigFingerprints(
input: EffectiveRunConfigFingerprintInput,
): EffectiveRunConfigFingerprints {
return {
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
categories: EFFECTIVE_RUN_CONFIG_FINGERPRINT_CATEGORIES,
sessionFingerprint: createCategoryFingerprint({
category: "session",
value: input.session,
secretManifest: input.secretManifest,
}),
workspaceFingerprint: createCategoryFingerprint({
category: "workspace",
value: input.workspace,
secretManifest: input.secretManifest,
}),
leaseFingerprint: createCategoryFingerprint({
category: "lease",
value: input.lease,
secretManifest: input.secretManifest,
}),
};
}
function fingerprintForCategory(
fingerprints: EffectiveRunConfigFingerprints,
category: EffectiveRunConfigFingerprintCategory,
) {
switch (category) {
case "session":
return fingerprints.sessionFingerprint.fingerprint;
case "workspace":
return fingerprints.workspaceFingerprint.fingerprint;
case "lease":
return fingerprints.leaseFingerprint.fingerprint;
}
}
export function diffEffectiveRunConfigFingerprints(
previous: EffectiveRunConfigFingerprints,
next: EffectiveRunConfigFingerprints,
): EffectiveRunConfigFingerprintDiff {
const changed = Object.fromEntries(
EFFECTIVE_RUN_CONFIG_FINGERPRINT_CATEGORIES.map((category) => [
category,
fingerprintForCategory(previous, category) !== fingerprintForCategory(next, category),
]),
) as Record<EffectiveRunConfigChangedCategory, boolean>;
const changedCategories = EFFECTIVE_RUN_CONFIG_FINGERPRINT_CATEGORIES.filter((category) => changed[category]);
return {
version: EFFECTIVE_RUN_CONFIG_FINGERPRINT_VERSION,
hasChanges: changedCategories.length > 0,
changedCategories,
changed,
};
}
+313 -48
View File
@@ -1,7 +1,7 @@
import { createHash, randomUUID } from "node:crypto";
import { and, eq, inArray } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { environmentLeases } from "@paperclipai/db";
import { companySecrets, companySecretVersions, environmentLeases } from "@paperclipai/db";
import type {
Environment,
EnvironmentLease,
@@ -18,10 +18,16 @@ import type {
import { ensureSshWorkspaceReady } from "@paperclipai/adapter-utils/ssh";
import { environmentService } from "./environments.js";
import {
collectEnvironmentSecretRefs,
parseEnvironmentDriverConfig,
resolveEnvironmentDriverConfigForRuntime,
stripSandboxProviderEnvelope,
} from "./environment-config.js";
import {
createEffectiveRunConfigFingerprints,
type EffectiveRunConfigFingerprint,
type EffectiveRunConfigSecretVersionMetadata,
} from "./effective-run-config-fingerprints.js";
import {
acquireSandboxProviderLease,
destroySandboxProviderLease,
@@ -255,6 +261,146 @@ function reusableRuntimeFingerprint(input: {
.digest("hex");
}
function serializeLeaseFingerprint(
fingerprint: EffectiveRunConfigFingerprint | null | undefined,
): Record<string, unknown> | null {
if (!fingerprint) return null;
return {
version: fingerprint.version,
category: fingerprint.category,
algorithm: fingerprint.algorithm,
fingerprint: fingerprint.fingerprint,
};
}
function readLeaseFingerprint(value: unknown): string | null {
return isRecord(value) ? readString(value.fingerprint) : null;
}
async function buildEnvironmentSecretMetadataForLeaseFingerprint(input: {
db: Db;
companyId: string;
environment: Environment;
}): Promise<EffectiveRunConfigSecretVersionMetadata[]> {
const refs = await collectEnvironmentSecretRefs({
db: input.db,
environment: input.environment,
});
if (refs.length === 0) return [];
const secretIds = [...new Set(refs.map((ref) => ref.secretId))];
const secretRows = await input.db
.select()
.from(companySecrets)
.where(inArray(companySecrets.id, secretIds));
const secretsById = new Map(
secretRows
.filter((secret) => secret.companyId === input.companyId)
.map((secret) => [secret.id, secret]),
);
const versionRequests = refs.flatMap((ref) => {
const secret = secretsById.get(ref.secretId);
if (!secret) return [];
const resolvedVersion = ref.versionSelector === "latest" || ref.versionSelector === undefined
? secret.latestVersion
: ref.versionSelector;
return typeof resolvedVersion === "number"
? [{ secretId: secret.id, version: resolvedVersion }]
: [];
});
const versionSecretIds = [...new Set(versionRequests.map((request) => request.secretId))];
const versions = [...new Set(versionRequests.map((request) => request.version))];
const versionRows = versionSecretIds.length > 0 && versions.length > 0
? await input.db
.select()
.from(companySecretVersions)
.where(
and(
inArray(companySecretVersions.secretId, versionSecretIds),
inArray(companySecretVersions.version, versions),
),
)
: [];
const versionsBySecretAndNumber = new Map(
versionRows.map((row) => [`${row.secretId}:${row.version}`, row]),
);
const metadata: EffectiveRunConfigSecretVersionMetadata[] = [];
for (const ref of refs) {
const secret = secretsById.get(ref.secretId);
if (!secret) {
metadata.push({
configPath: ref.configPath,
envKey: null,
secretId: ref.secretId,
version: typeof ref.versionSelector === "number" ? ref.versionSelector : "unresolved",
outcome: "failure",
});
continue;
}
const resolvedVersion = ref.versionSelector === "latest" || ref.versionSelector === undefined
? secret.latestVersion
: ref.versionSelector;
const versionRow = typeof resolvedVersion === "number"
? versionsBySecretAndNumber.get(`${secret.id}:${resolvedVersion}`) ?? null
: null;
metadata.push({
configPath: ref.configPath,
envKey: null,
secretId: secret.id,
version: resolvedVersion,
provider: secret.provider,
providerVersionRef: versionRow?.providerVersionRef ?? null,
outcome: versionRow ? "success" : "failure",
});
}
return metadata;
}
async function buildReusableSandboxLeaseFingerprint(input: {
db: Db;
companyId: string;
environment: Environment;
executionWorkspaceId: string | null;
agentId: string | null;
adapterType: string | null;
provider: string;
providerConfig: Record<string, unknown>;
providerPlugin?: {
id: string;
pluginKey: string;
packageName: string;
version: string;
} | null;
}): Promise<EffectiveRunConfigFingerprint> {
const secretMetadata = await buildEnvironmentSecretMetadataForLeaseFingerprint({
db: input.db,
companyId: input.companyId,
environment: input.environment,
});
return createEffectiveRunConfigFingerprints({
lease: {
companyId: input.companyId,
environment: {
id: input.environment.id,
driver: input.environment.driver,
},
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: input.provider,
providerPlugin: input.providerPlugin ?? null,
providerConfig: input.providerConfig,
secrets: secretMetadata,
},
secretManifest: secretMetadata,
}).leaseFingerprint;
}
function buildReusableSandboxLeaseScope(input: {
companyId: string;
environmentId: string;
@@ -263,6 +409,7 @@ function buildReusableSandboxLeaseScope(input: {
adapterType: string | null;
provider: string;
config: Record<string, unknown>;
leaseFingerprint?: EffectiveRunConfigFingerprint | null;
providerMetadata?: Record<string, unknown> | null;
}): Record<string, unknown> | null {
if (!input.executionWorkspaceId || !input.agentId) return null;
@@ -285,6 +432,9 @@ function buildReusableSandboxLeaseScope(input: {
adapterType,
config: input.config,
}),
...(input.leaseFingerprint
? { leaseFingerprint: serializeLeaseFingerprint(input.leaseFingerprint) }
: {}),
...(remoteCwd ? { remoteCwd } : {}),
...(workspaceSentinel ? { workspaceSentinel } : {}),
};
@@ -299,24 +449,48 @@ function reusableSandboxLeaseScopeMatches(input: {
adapterType: string | null;
provider: string;
config: Record<string, unknown>;
leaseFingerprint?: EffectiveRunConfigFingerprint | null;
allowLegacyRuntimeFingerprint?: boolean;
}): boolean {
if (!input.executionWorkspaceId || !input.agentId) return false;
const scope = input.lease.metadata?.reusableSandboxLease;
if (!isRecord(scope)) return false;
const adapterType = input.adapterType ?? null;
return (
const baseScopeMatches =
scope.companyId === input.companyId &&
scope.environmentId === input.environmentId &&
scope.executionWorkspaceId === input.executionWorkspaceId &&
scope.agentId === input.agentId &&
scope.adapterType === adapterType &&
scope.provider === input.provider &&
scope.runtimeFingerprint === reusableRuntimeFingerprint({
provider: input.provider,
adapterType,
config: input.config,
})
);
scope.provider === input.provider;
if (!baseScopeMatches) return false;
const expectedLeaseFingerprint = input.leaseFingerprint?.fingerprint ?? null;
if (expectedLeaseFingerprint) {
const storedLeaseFingerprint = readLeaseFingerprint(scope.leaseFingerprint);
if (storedLeaseFingerprint) {
return storedLeaseFingerprint === expectedLeaseFingerprint;
}
if (!input.allowLegacyRuntimeFingerprint) return false;
}
return scope.runtimeFingerprint === reusableRuntimeFingerprint({
provider: input.provider,
adapterType,
config: input.config,
});
}
function reusableLeaseCanBeResumed(input: {
lease: Pick<EnvironmentLease, "status" | "heartbeatRunId">;
heartbeatRunId: string | null;
}): boolean {
if (input.lease.status === "released" || input.lease.status === "retained") return true;
return input.lease.status === "active" && input.heartbeatRunId !== null && input.lease.heartbeatRunId === input.heartbeatRunId;
}
function reusableLeaseCanBeCleanedUp(lease: Pick<EnvironmentLease, "status">): boolean {
return lease.status === "released" || lease.status === "retained";
}
export function findReusableSandboxLeaseId(input: {
@@ -531,6 +705,23 @@ function createSandboxEnvironmentDriver(
};
}
async function cleanupObsoleteReusableSandboxLeases(input: {
environment: Environment;
leases: EnvironmentLease[];
reusableLeases: EnvironmentLease[];
}) {
const reusableIds = new Set(input.reusableLeases.map((lease) => lease.id));
for (const lease of input.leases) {
if (reusableIds.has(lease.id)) continue;
if (!reusableLeaseCanBeCleanedUp(lease)) continue;
await destroyReusableSandboxLease({
environment: input.environment,
lease,
failureReason: "lease_fingerprint_mismatch",
});
}
}
return {
driver: "sandbox",
@@ -573,14 +764,38 @@ function createSandboxEnvironmentDriver(
const workerConfig = stripSandboxProviderEnvelope(parsed.config);
const storedConfig = storedParsed.config;
const providerConfigForLease = sandboxConfigForLeaseMetadata(storedConfig);
const supportsReusableLeases = pluginProvider.resolved.driver.supportsReusableLeases === true;
const leaseFingerprint =
supportsReusableLeases &&
parsed.config.reuseLease &&
input.heartbeatRunId !== null &&
input.executionWorkspaceId !== null &&
input.agentId !== null
? await buildReusableSandboxLeaseFingerprint({
db,
companyId: input.companyId,
environment: input.environment,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
providerConfig: providerConfigForLease,
providerPlugin: {
id: pluginProvider.resolved.plugin.id,
pluginKey: pluginProvider.resolved.plugin.pluginKey,
packageName: pluginProvider.resolved.plugin.packageName,
version: pluginProvider.resolved.plugin.version,
},
})
: null;
// Ad-hoc tests (heartbeatRunId === null) must never resume an existing
// provider lease. If they did, releasing the test lease at the end of
// the probe would tear down the live heartbeat run that owns it.
// We also filter out leases whose policy is not reuse_by_environment
// and whose status is not reusable so non-reusable, cleanup-pending,
// or terminal rows cannot be matched.
const reusableExistingLeases =
const reusableCandidateLeases =
supportsReusableLeases &&
parsed.config.reuseLease &&
input.heartbeatRunId !== null &&
@@ -589,21 +804,35 @@ function createSandboxEnvironmentDriver(
? (await environmentsSvc.listLeases(input.environment.id))
.filter((lease) =>
lease.leasePolicy === "reuse_by_environment" &&
["active", "released", "retained"].includes(lease.status) &&
reusableLeaseCanBeResumed({ lease, heartbeatRunId: input.heartbeatRunId }) &&
lease.executionWorkspaceId === input.executionWorkspaceId &&
lease.metadata?.agentId === input.agentId &&
reusableSandboxLeaseScopeMatches({
lease,
companyId: input.companyId,
environmentId: input.environment.id,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: sandboxConfigForLeaseMetadata(storedConfig),
}),
lease.metadata?.agentId === input.agentId,
)
: [];
const reusableExistingLeases = reusableCandidateLeases.filter((lease) =>
reusableSandboxLeaseScopeMatches({
lease,
companyId: input.companyId,
environmentId: input.environment.id,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: providerConfigForLease,
leaseFingerprint,
allowLegacyRuntimeFingerprint:
lease.status === "active" &&
input.heartbeatRunId !== null &&
lease.heartbeatRunId === input.heartbeatRunId,
}),
);
if (reusableCandidateLeases.length > reusableExistingLeases.length) {
await cleanupObsoleteReusableSandboxLeases({
environment: input.environment,
leases: reusableCandidateLeases,
reusableLeases: reusableExistingLeases,
});
}
const reusableProviderLeaseId =
supportsReusableLeases &&
parsed.config.reuseLease &&
@@ -694,7 +923,8 @@ function createSandboxEnvironmentDriver(
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: sandboxConfigForLeaseMetadata(storedConfig),
config: providerConfigForLease,
leaseFingerprint,
providerMetadata: sanitizedProviderMetadata,
})
: null;
@@ -730,38 +960,72 @@ function createSandboxEnvironmentDriver(
// so non-reusable, cleanup-pending, or terminal rows can never be matched.
const builtinSandboxProvider = getBuiltinSandboxProvider(parsed.config.provider);
const supportsReusableLeases = builtinSandboxProvider?.supportsReusableLeases === true;
const providerConfigForLease = sandboxConfigForLeaseMetadata(parsed.config);
const leaseFingerprint =
supportsReusableLeases &&
parsed.config.reuseLease &&
input.heartbeatRunId !== null &&
input.executionWorkspaceId !== null &&
input.agentId !== null
? await buildReusableSandboxLeaseFingerprint({
db,
companyId: input.companyId,
environment: input.environment,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
providerConfig: providerConfigForLease,
})
: null;
const reusableCandidateLeases =
supportsReusableLeases &&
parsed.config.reuseLease &&
input.heartbeatRunId !== null &&
input.executionWorkspaceId !== null &&
input.agentId !== null
? (await environmentsSvc.listLeases(input.environment.id))
.filter((lease) =>
lease.leasePolicy === "reuse_by_environment" &&
reusableLeaseCanBeResumed({ lease, heartbeatRunId: input.heartbeatRunId }) &&
lease.executionWorkspaceId === input.executionWorkspaceId &&
lease.metadata?.agentId === input.agentId,
)
: [];
const reusableExistingLeases = reusableCandidateLeases.filter((lease) =>
reusableSandboxLeaseScopeMatches({
lease,
companyId: input.companyId,
environmentId: input.environment.id,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: providerConfigForLease,
leaseFingerprint,
allowLegacyRuntimeFingerprint:
lease.status === "active" &&
input.heartbeatRunId !== null &&
lease.heartbeatRunId === input.heartbeatRunId,
}),
);
if (reusableCandidateLeases.length > reusableExistingLeases.length) {
await cleanupObsoleteReusableSandboxLeases({
environment: input.environment,
leases: reusableCandidateLeases,
reusableLeases: reusableExistingLeases,
});
}
const reusableProviderLeaseId =
supportsReusableLeases &&
parsed.config.reuseLease &&
input.heartbeatRunId !== null &&
input.executionWorkspaceId !== null &&
input.agentId !== null
? (await environmentsSvc
.listLeases(input.environment.id)
.then((leases) =>
findReusableSandboxLeaseId({
config: parsed.config,
leases: leases.filter((lease) =>
lease.leasePolicy === "reuse_by_environment" &&
["active", "released", "retained"].includes(lease.status) &&
lease.executionWorkspaceId === input.executionWorkspaceId &&
lease.metadata?.agentId === input.agentId &&
reusableSandboxLeaseScopeMatches({
lease,
companyId: input.companyId,
environmentId: input.environment.id,
executionWorkspaceId: input.executionWorkspaceId,
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: sandboxConfigForLeaseMetadata(parsed.config),
}),
),
}),
))
? findReusableSandboxLeaseId({ config: parsed.config, leases: reusableExistingLeases })
: null;
const reusableLease = reusableProviderLeaseId
? (await environmentsSvc.listLeases(input.environment.id)).find((lease) => lease.providerLeaseId === reusableProviderLeaseId)
? reusableExistingLeases.find((lease) => lease.providerLeaseId === reusableProviderLeaseId)
: null;
let providerLease;
@@ -806,7 +1070,8 @@ function createSandboxEnvironmentDriver(
agentId: input.agentId,
adapterType: input.adapterType,
provider: parsed.config.provider,
config: sandboxConfigForLeaseMetadata(parsed.config),
config: providerConfigForLease,
leaseFingerprint,
providerMetadata: providerLease.metadata,
})
: null;
File diff suppressed because it is too large. Load diff
+2
View File
@@ -244,6 +244,7 @@ export type RuntimeSecretManifestEntry = {
secretKey: string;
version: number;
provider: SecretProvider;
providerVersionRef?: string | null;
outcome: "success" | "failure";
errorCode?: string | null;
};
@@ -696,6 +697,7 @@ export function secretService(db: Db) {
secretKey: secret.key,
version: resolvedVersion,
provider: providerId,
providerVersionRef: versionRow.providerVersionRef,
outcome: "success",
},
};
+3
View File
@@ -1471,6 +1471,9 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input: {
const provisionCommand = asString(input.workspace.config?.provisionCommand, "").trim();
if (strategy !== "git_worktree") {
if (!await directoryExists(cwd)) {
return null;
}
return realized;
}
const repoRoot = await runGit(["rev-parse", "--show-toplevel"], input.base.baseCwd);