Merge master into agent configuration redesign

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-22 19:30:26 +00:00
commit e2b4bb6009
23 files changed
+1369 -62

No files matched your search

@@ -100,17 +100,6 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () =
}, 20_000);
afterEach(async () => {
mockAdapterExecute.mockReset();
mockAdapterExecute.mockImplementation(async () => ({
exitCode: 0,
signal: null,
timedOut: false,
errorMessage: null,
summary: "Dependency-aware heartbeat test run.",
provider: "test",
model: "test-model",
}));
runningProcesses.clear();
let idlePolls = 0;
for (let attempt = 0; attempt < 100; attempt += 1) {
const runs = await db
@@ -125,7 +114,22 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () =
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
await new Promise((resolve) => setTimeout(resolve, 50));
const runIds = await db
.select({ id: heartbeatRuns.id })
.from(heartbeatRuns)
.then((runs) => runs.map((run) => run.id));
await Promise.all(runIds.map((runId) => heartbeat.waitForRunExecutionDrain(runId)));
mockAdapterExecute.mockReset();
mockAdapterExecute.mockImplementation(async () => ({
exitCode: 0,
signal: null,
timedOut: false,
errorMessage: null,
summary: "Dependency-aware heartbeat test run.",
provider: "test",
model: "test-model",
}));
runningProcesses.clear();
await db.delete(environmentLeases);
await db.delete(activityLog);
await db.delete(companySkills);
@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from "vitest";
import type { heartbeatRuns } from "@paperclipai/db";
import { resolveLedgerScopeForRun } from "../services/heartbeat.ts";
type IssueRow = { id: string; projectId: string | null; billingCode: string | null };
/**
* Minimal Drizzle stand-in for the single lookup `resolveLedgerScopeForRun`
* performs: `db.select({...}).from(issues).where(...)` resolved as a thenable.
*/
type LedgerDb = Parameters<typeof resolveLedgerScopeForRun>[0];
function makeDb(rows: IssueRow[]) {
const where = vi.fn(() => Promise.resolve(rows));
const from = vi.fn(() => ({ where }));
const select = vi.fn(() => ({ from }));
return { db: { select } as unknown as LedgerDb, select, from, where };
}
function makeRun(contextSnapshot: Record<string, unknown>) {
return { id: "run-1", contextSnapshot } as unknown as typeof heartbeatRuns.$inferSelect;
}
describe("resolveLedgerScopeForRun billing code propagation", () => {
it("carries the issue's billing code onto the ledger scope", async () => {
const { db } = makeDb([{ id: "issue-1", projectId: "project-1", billingCode: "ACME-42" }]);
const scope = await resolveLedgerScopeForRun(db, "company-1", makeRun({
issueId: "issue-1",
projectId: "context-project",
}));
expect(scope).toEqual({
issueId: "issue-1",
projectId: "project-1",
billingCode: "ACME-42",
});
});
it("resolves a null billing code when the issue has none set", async () => {
const { db } = makeDb([{ id: "issue-1", projectId: "project-1", billingCode: null }]);
const scope = await resolveLedgerScopeForRun(db, "company-1", makeRun({
issueId: "issue-1",
projectId: "context-project",
}));
expect(scope.billingCode).toBeNull();
expect(scope.issueId).toBe("issue-1");
});
it("resolves a null billing code without querying when the run has no issue in context", async () => {
const { db, select } = makeDb([]);
const scope = await resolveLedgerScopeForRun(db, "company-1", makeRun({
projectId: "context-project",
}));
expect(scope).toEqual({
issueId: null,
projectId: "context-project",
billingCode: null,
});
expect(select).not.toHaveBeenCalled();
});
it("resolves a null billing code when the issue is not visible to the company", async () => {
const { db } = makeDb([]);
const scope = await resolveLedgerScopeForRun(db, "other-company", makeRun({
issueId: "issue-1",
projectId: "context-project",
}));
expect(scope).toEqual({
issueId: null,
projectId: "context-project",
billingCode: null,
});
});
});
@@ -103,6 +103,28 @@ export async function resolveEnvironmentExecutionTarget(input: {
startedAt,
};
},
// Expose the native file-sync capability only when the provider's
// worker advertises BOTH sync verbs; otherwise leave syncIn/syncOut
// undefined so the orchestrator keeps the byte-identical base64 path.
...(input.environmentRuntime.supportsSync({
environment: input.environment as Environment,
lease: input.lease,
})
? {
syncIn: (operations) =>
input.environmentRuntime!.syncIn({
environment: input.environment as Environment,
lease: input.lease!,
operations,
}),
syncOut: (operations) =>
input.environmentRuntime!.syncOut({
environment: input.environment as Environment,
lease: input.lease!,
operations,
}),
}
: {}),
}
: undefined,
};
@@ -14,6 +14,8 @@ import type {
PluginEnvironmentExecuteResult,
PluginEnvironmentLease,
PluginEnvironmentRealizeWorkspaceResult,
PluginEnvironmentSyncResult,
PluginSyncOperation,
} from "@paperclipai/plugin-sdk";
import { ensureSshWorkspaceReady } from "@paperclipai/adapter-utils/ssh";
import { environmentService } from "./environments.js";
@@ -185,6 +187,10 @@ export interface EnvironmentDriverExecuteInput extends EnvironmentDriverLeaseInp
timeoutMs?: number;
}
export interface EnvironmentDriverSyncInput extends EnvironmentDriverLeaseInput {
operations: PluginSyncOperation[];
}
export interface EnvironmentRuntimeDriver {
readonly driver: string;
acquireRunLease(input: EnvironmentDriverAcquireInput): Promise<EnvironmentLease>;
@@ -193,6 +199,16 @@ export interface EnvironmentRuntimeDriver {
destroyRunLease?(input: EnvironmentDriverLeaseInput): Promise<EnvironmentLease | null>;
realizeWorkspace?(input: EnvironmentDriverRealizeWorkspaceInput): Promise<PluginEnvironmentRealizeWorkspaceResult>;
execute?(input: EnvironmentDriverExecuteInput): Promise<PluginEnvironmentExecuteResult>;
/**
* Optional native inbound/outbound file transfer, delegated to the plugin
* worker's `environmentSyncIn`/`environmentSyncOut` verbs. Only present for
* plugin-backed sandbox drivers whose worker advertises both verbs; callers
* gate on {@link EnvironmentRuntimeDriver.supportsSync}.
*/
syncIn?(input: EnvironmentDriverSyncInput): Promise<PluginEnvironmentSyncResult>;
syncOut?(input: EnvironmentDriverSyncInput): Promise<PluginEnvironmentSyncResult>;
/** True when the lease's plugin worker advertises both sync verbs. */
supportsSync?(input: EnvironmentDriverLeaseInput): boolean;
}
export interface EnvironmentRuntimeLeaseRecord {
@@ -722,6 +738,39 @@ function createSandboxEnvironmentDriver(
}
}
async function callPluginEnvironmentSync(
method: "environmentSyncIn" | "environmentSyncOut",
input: EnvironmentDriverSyncInput,
): Promise<PluginEnvironmentSyncResult> {
if (!input.lease.metadata?.sandboxProviderPlugin || !pluginWorkerManager) {
throw new Error("Sandbox driver does not support native file sync for this lease.");
}
const pluginId = readString(input.lease.metadata?.pluginId);
const providerKey = readString(input.lease.metadata?.provider);
if (!pluginId || !providerKey) {
throw new Error("Sandbox lease is missing plugin/provider metadata for native file sync.");
}
const config = await resolvePluginSandboxRuntimeConfig({
environment: input.environment,
lease: input.lease,
provider: providerKey,
});
const sanitizedConfig = stripSandboxProviderEnvelope(config as SandboxEnvironmentConfig);
return await pluginWorkerManager.call(pluginId, method, {
driverKey: providerKey,
companyId: input.lease.companyId,
environmentId: input.environment.id,
issueId: input.lease.issueId,
config: sanitizedConfig,
lease: {
providerLeaseId: input.lease.providerLeaseId,
metadata: input.lease.metadata ?? undefined,
expiresAt: input.lease.expiresAt?.toISOString() ?? null,
},
operations: input.operations,
}, resolvePluginSandboxRpcTimeoutMs(sanitizedConfig));
}
return {
driver: "sandbox",
@@ -1235,6 +1284,22 @@ function createSandboxEnvironmentDriver(
throw new Error("Sandbox driver does not support direct command execution for built-in providers.");
},
supportsSync(input) {
if (!input.lease.metadata?.sandboxProviderPlugin || !pluginWorkerManager) return false;
const pluginId = readString(input.lease.metadata?.pluginId);
if (!pluginId) return false;
const advertised = pluginWorkerManager.getWorker(pluginId)?.supportedMethods ?? [];
return advertised.includes("environmentSyncIn") && advertised.includes("environmentSyncOut");
},
async syncIn(input) {
return await callPluginEnvironmentSync("environmentSyncIn", input);
},
async syncOut(input) {
return await callPluginEnvironmentSync("environmentSyncOut", input);
},
async destroyRunLease(input) {
return await destroyReusableSandboxLease({
environment: input.environment,
@@ -1890,6 +1955,27 @@ export function environmentRuntimeService(
}
return await driver.execute(input);
},
supportsSync(input: EnvironmentDriverLeaseInput): boolean {
const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
return driver?.supportsSync?.(input) ?? false;
},
async syncIn(input: EnvironmentDriverSyncInput): Promise<PluginEnvironmentSyncResult> {
const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
if (!driver.syncIn) {
throw new Error(`Environment driver "${driver.driver}" does not support native file sync.`);
}
return await driver.syncIn(input);
},
async syncOut(input: EnvironmentDriverSyncInput): Promise<PluginEnvironmentSyncResult> {
const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
if (!driver.syncOut) {
throw new Error(`Environment driver "${driver.driver}" does not support native file sync.`);
}
return await driver.syncOut(input);
},
};
}
+5 -1
View File
@@ -2667,7 +2667,7 @@ export function resolveLedgerCostStatus(input: {
return input.costUsd == null && hasTokenUsage ? "unpriced" : "reported";
}
async function resolveLedgerScopeForRun(
export async function resolveLedgerScopeForRun(
db: Db,
companyId: string,
run: typeof heartbeatRuns.$inferSelect,
@@ -2680,6 +2680,7 @@ async function resolveLedgerScopeForRun(
return {
issueId: null,
projectId: contextProjectId,
billingCode: null,
};
}
@@ -2687,6 +2688,7 @@ async function resolveLedgerScopeForRun(
.select({
id: issues.id,
projectId: issues.projectId,
billingCode: issues.billingCode,
})
.from(issues)
.where(and(eq(issues.id, contextIssueId), eq(issues.companyId, companyId)))
@@ -2695,6 +2697,7 @@ async function resolveLedgerScopeForRun(
return {
issueId: issue?.id ?? null,
projectId: issue?.projectId ?? contextProjectId,
billingCode: issue?.billingCode ?? null,
};
}
@@ -11653,6 +11656,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
agentId: agent.id,
issueId: ledgerScope.issueId,
projectId: ledgerScope.projectId,
billingCode: ledgerScope.billingCode,
provider,
biller,
billingType,