mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Bridge workers keep startup setup separate from long-lived runtime
work
> - The callback bridge must keep queue-directory setup inside the
startup step
> - The long-lived poll loop must run with no active startup step
> - This pull request keeps that boundary in the right place
> - The benefit is correct span parents and correct runtime exec
metadata
## Linked Issues or Issue Description
**Bug**
**What happened?**
Long-lived bridge continuations kept a stale startup step store during
the queue-directory setup path.
**Expected behavior**
Runtime exec spans should start with no active startup step.
**Steps to reproduce**
1. Start a bridge lane.
2. Let the startup step end.
3. Run later runtime exec work on the same lane.
**Paperclip version or commit**
223068e2ff
**Deployment mode**
Self-hosted server
## What Changed
- Added `runWithoutActiveStep` in
`packages/adapter-utils/src/acpx-engine/startup-timing.ts`.
- Wrapped the long-lived poll timer, socket handlers, and
callback-bridge worker loop in both bridge lanes.
- Added unit tests for store leak and reset behavior.
- Added continuation tests for both bridge lanes and the `criticalPath`
flag.
## Verification
- `pnpm --filter @paperclipai/adapter-utils exec tsc --noEmit`
- `pnpm exec vitest run
packages/adapter-utils/src/acpx-engine/startup-timing.test.ts`
- `pnpm exec vitest run
server/src/__tests__/environment-execution-target.test.ts`
## Risks
- Low risk.
- The change alters async context handling in bridge continuations.
- If a caller depends on inherited step state, this change removes it.
- The tests cover the intended bridge lanes.
## Model Used
OpenAI Codex, GPT-5, tool use enabled.
## 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>
964 lines
33 KiB
TypeScript
964 lines
33 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { mockResolveEnvironmentDriverConfigForRuntime } = vi.hoisted(() => ({
|
|
mockResolveEnvironmentDriverConfigForRuntime: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../services/environment-config.js", () => ({
|
|
resolveEnvironmentDriverConfigForRuntime: mockResolveEnvironmentDriverConfigForRuntime,
|
|
}));
|
|
|
|
import {
|
|
measureStartupStep,
|
|
runWithoutActiveStep,
|
|
SANDBOX_STARTUP_SPAN_ATTRS,
|
|
} from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
|
|
import {
|
|
DEFAULT_SANDBOX_REMOTE_CWD,
|
|
resolveEnvironmentExecutionTarget,
|
|
} from "../services/environment-execution-target.js";
|
|
|
|
const A = SANDBOX_STARTUP_SPAN_ATTRS;
|
|
|
|
// A recording trace context that models the OTel parenting contract:
|
|
// `startSpan(name, options, context)` reads the parent from the explicit
|
|
// `context` token that `contextWithSpan` built. A test asserts the exact parent
|
|
// of each child span without an OTel package.
|
|
function createRecordingTrace() {
|
|
const spans: Array<{
|
|
name: string;
|
|
attributes: Record<string, unknown>;
|
|
parent: unknown;
|
|
ended: boolean;
|
|
setAttribute(key: string, value: unknown): void;
|
|
end(): void;
|
|
}> = [];
|
|
const tracer = {
|
|
startSpan(name: string, _options?: unknown, context?: unknown) {
|
|
const parent =
|
|
context && typeof context === "object" && "span" in context
|
|
? (context as { span: unknown }).span
|
|
: null;
|
|
const span = {
|
|
name,
|
|
attributes: {} as Record<string, unknown>,
|
|
parent,
|
|
ended: false,
|
|
setAttribute(key: string, value: unknown) {
|
|
span.attributes[key] = value;
|
|
},
|
|
end() {
|
|
span.ended = true;
|
|
},
|
|
};
|
|
spans.push(span);
|
|
return span;
|
|
},
|
|
};
|
|
const contextWithSpan = (span: unknown) => ({ span });
|
|
return { tracer, contextWithSpan, spans };
|
|
}
|
|
|
|
describe("resolveEnvironmentExecutionTarget", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
delete process.env.PAPERCLIP_API_URL;
|
|
delete process.env.PAPERCLIP_RUNTIME_API_URL;
|
|
});
|
|
|
|
it("uses a bounded default cwd for sandbox targets when lease metadata omits remoteCwd", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: {
|
|
id: "env-1",
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
},
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
providerKey: "fake-plugin",
|
|
remoteCwd: DEFAULT_SANDBOX_REMOTE_CWD,
|
|
leaseId: "lease-1",
|
|
environmentId: "env-1",
|
|
timeoutMs: 30_000,
|
|
});
|
|
});
|
|
|
|
it("keeps sandbox targets on bridge mode even when lease metadata includes a Paperclip API URL", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: {
|
|
id: "env-1",
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
},
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {
|
|
paperclipApiUrl: "https://paperclip.example.test",
|
|
},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
providerKey: "fake-plugin",
|
|
remoteCwd: DEFAULT_SANDBOX_REMOTE_CWD,
|
|
});
|
|
expect(target).not.toHaveProperty("paperclipApiUrl");
|
|
expect(target).not.toHaveProperty("paperclipTransport");
|
|
});
|
|
|
|
it("passes through a provider-declared sandbox shell command from lease metadata", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "claude_local",
|
|
environment: {
|
|
id: "env-1",
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
},
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {
|
|
shellCommand: "bash",
|
|
},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
shellCommand: "bash",
|
|
});
|
|
});
|
|
|
|
it("keeps sandbox targets on callback bridge execution even when lease metadata advertises SSH access", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "claude_local",
|
|
environment: {
|
|
id: "env-1",
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
},
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {
|
|
remoteCwd: "/home/sandbox/paperclip-workspace",
|
|
sshAccess: {
|
|
type: "ssh",
|
|
host: "ssh.example.test",
|
|
port: 22,
|
|
username: "paperclip",
|
|
},
|
|
},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
providerKey: "fake-plugin",
|
|
remoteCwd: "/home/sandbox/paperclip-workspace",
|
|
});
|
|
});
|
|
|
|
it("resolves sandbox targets for every remote-managed adapter, including grok_local", async () => {
|
|
for (const adapterType of [
|
|
"claude_local",
|
|
"codex_local",
|
|
"cursor",
|
|
"gemini_local",
|
|
"grok_local",
|
|
"opencode_local",
|
|
"pi_local",
|
|
]) {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType,
|
|
environment: {
|
|
id: "env-1",
|
|
driver: "sandbox",
|
|
config: { provider: "fake-plugin" },
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target, `adapter ${adapterType}`).toMatchObject({
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
providerKey: "fake-plugin",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("returns null for adapters without remote-managed environment support", async () => {
|
|
for (const driver of ["sandbox", "ssh"] as const) {
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "process",
|
|
environment: {
|
|
id: "env-1",
|
|
driver,
|
|
config: { provider: "fake-plugin" },
|
|
},
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target, `driver ${driver}`).toBeNull();
|
|
}
|
|
expect(mockResolveEnvironmentDriverConfigForRuntime).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves SSH execution targets for grok_local", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "ssh",
|
|
config: {
|
|
host: "ssh.example.test",
|
|
port: 22,
|
|
username: "paperclip",
|
|
remoteWorkspacePath: "/srv/paperclip",
|
|
privateKey: "PRIVATE KEY",
|
|
knownHosts: "[ssh.example.test]:22 ssh-ed25519 AAAA",
|
|
strictHostKeyChecking: true,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "grok_local",
|
|
environment: {
|
|
id: "env-ssh-1",
|
|
driver: "ssh",
|
|
config: {},
|
|
},
|
|
leaseId: "lease-ssh-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "ssh",
|
|
remoteCwd: "/srv/paperclip",
|
|
});
|
|
});
|
|
|
|
it("resolves SSH execution targets in bridge mode", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "ssh",
|
|
config: {
|
|
host: "ssh.example.test",
|
|
port: 22,
|
|
username: "paperclip",
|
|
remoteWorkspacePath: "/srv/paperclip",
|
|
privateKey: "PRIVATE KEY",
|
|
knownHosts: "[ssh.example.test]:22 ssh-ed25519 AAAA",
|
|
strictHostKeyChecking: true,
|
|
},
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: {
|
|
id: "env-ssh-1",
|
|
driver: "ssh",
|
|
config: {},
|
|
},
|
|
leaseId: "lease-ssh-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target).toMatchObject({
|
|
kind: "remote",
|
|
transport: "ssh",
|
|
remoteCwd: "/srv/paperclip",
|
|
leaseId: "lease-ssh-1",
|
|
environmentId: "env-ssh-1",
|
|
spec: {
|
|
host: "ssh.example.test",
|
|
port: 22,
|
|
username: "paperclip",
|
|
remoteWorkspacePath: "/srv/paperclip",
|
|
remoteCwd: "/srv/paperclip",
|
|
},
|
|
});
|
|
expect(target).not.toHaveProperty("paperclipApiUrl");
|
|
});
|
|
|
|
it("exposes a sandbox runner with single-stream stdin upload disabled", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: {
|
|
provider: "fake-plugin",
|
|
reuseLease: false,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
const environmentRuntime = {
|
|
execute: vi.fn().mockResolvedValue({
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "ok",
|
|
stderr: "",
|
|
metadata: { durationMs: 600, getDurationMs: 15 },
|
|
}),
|
|
supportsSync: vi.fn().mockReturnValue(false),
|
|
};
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "fake-plugin" } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: { remoteCwd: "/workspace" },
|
|
lease: { id: "lease-1" } as never,
|
|
environmentRuntime: environmentRuntime as never,
|
|
});
|
|
|
|
const runner = (target as { runner?: {
|
|
supportsSingleStreamStdinProgress?: boolean;
|
|
execute(input: { command: string; args?: string[] }): Promise<unknown>;
|
|
} }).runner;
|
|
expect(runner).toBeTruthy();
|
|
// Provider-backed sandbox RPCs do not surface bounded mid-stream progress
|
|
// for a single stdin upload, so the runner leaves the capability disabled.
|
|
expect(runner!.supportsSingleStreamStdinProgress).toBe(false);
|
|
|
|
// The exec seam still runs each command; the run-log no longer carries the
|
|
// detailed per-step round-trip or provider-duration counts.
|
|
await runner!.execute({ command: "echo", args: ["a"] });
|
|
await runner!.execute({ command: "echo", args: ["b"] });
|
|
expect(environmentRuntime.execute).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
// A recording tracer that captures each provider-exec span's name, attribute
|
|
// map, and end. It satisfies the structural tracer the seam calls.
|
|
function createRecordingExecTracer() {
|
|
const spans: Array<{
|
|
name: string;
|
|
attributes: Record<string, unknown>;
|
|
status: { code: number; message?: string } | null;
|
|
ended: boolean;
|
|
}> = [];
|
|
const tracer = {
|
|
startSpan(name: string) {
|
|
const span = {
|
|
name,
|
|
attributes: {} as Record<string, unknown>,
|
|
status: null as { code: number; message?: string } | null,
|
|
ended: false,
|
|
setAttribute(key: string, value: unknown) {
|
|
span.attributes[key] = value;
|
|
},
|
|
setStatus(status: { code: number; message?: string }) {
|
|
span.status = status;
|
|
},
|
|
end() {
|
|
span.ended = true;
|
|
},
|
|
};
|
|
spans.push(span);
|
|
return span;
|
|
},
|
|
};
|
|
return { tracer, spans };
|
|
}
|
|
|
|
// The value of `SpanStatusCode.ERROR` in `@opentelemetry/api`. A failed exec
|
|
// span must carry this native status, not only the `failed` outcome attribute.
|
|
const SPAN_STATUS_CODE_ERROR = 2;
|
|
|
|
// The closed span-attribute allowlist for a `sandbox.exec` span. A test
|
|
// asserts every recorded key is in this set, so a command, an argument, a
|
|
// path, an id, or an error-text key can never ride the span.
|
|
const ALLOWED_EXEC_SPAN_ATTRIBUTE_KEYS = new Set<string>([
|
|
A.provider,
|
|
A.execCommand,
|
|
A.execExitCode,
|
|
A.execWallMs,
|
|
A.execWaitBeforeMs,
|
|
A.execSandboxMs,
|
|
A.execNetworkMs,
|
|
A.execCriticalPath,
|
|
A.execCacheHit,
|
|
A.outcome,
|
|
]);
|
|
|
|
async function runnerFor(input: {
|
|
provider: string;
|
|
execResult: Record<string, unknown>;
|
|
tracer: unknown;
|
|
}) {
|
|
return runnerWithExecute({
|
|
provider: input.provider,
|
|
tracer: input.tracer,
|
|
execute: vi.fn().mockResolvedValue(input.execResult),
|
|
});
|
|
}
|
|
|
|
// Build the sandbox runner with a custom provider-exec implementation, so a
|
|
// test can drive a thrown execution or assert the span order around the await.
|
|
async function runnerWithExecute(input: {
|
|
provider: string;
|
|
tracer: unknown;
|
|
execute: (...args: unknown[]) => Promise<unknown>;
|
|
}) {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: input.provider, reuseLease: false, timeoutMs: 30_000 },
|
|
});
|
|
const environmentRuntime = {
|
|
execute: input.execute,
|
|
supportsSync: vi.fn().mockReturnValue(false),
|
|
};
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: input.provider } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: { remoteCwd: "/workspace" },
|
|
lease: { id: "lease-1" } as never,
|
|
environmentRuntime: environmentRuntime as never,
|
|
tracer: input.tracer as never,
|
|
});
|
|
return (target as { runner?: {
|
|
execute(input: { command: string; args?: string[] }): Promise<unknown>;
|
|
} }).runner!;
|
|
}
|
|
|
|
it("sets the provider duration attributes from finite Daytona-shaped metadata", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: {
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "ok",
|
|
stderr: "",
|
|
metadata: { durationMs: 600, getDurationMs: 15 },
|
|
},
|
|
tracer,
|
|
});
|
|
|
|
await runner.execute({ command: "echo", args: ["a"] });
|
|
|
|
expect(spans).toHaveLength(1);
|
|
const span = spans[0]!;
|
|
expect(span.name).toBe("sandbox.exec");
|
|
expect(span.ended).toBe(true);
|
|
// `sandbox_ms` = provider in-sandbox run; `wait_before_ms` = handle-fetch.
|
|
expect(span.attributes[A.execSandboxMs]).toBe(600);
|
|
expect(span.attributes[A.execWaitBeforeMs]).toBe(15);
|
|
expect(span.attributes[A.provider]).toBe("daytona");
|
|
// `echo` is a known command basename, so it rides as a clamped label.
|
|
expect(span.attributes[A.execCommand]).toBe("echo");
|
|
expect(span.attributes[A.execExitCode]).toBe(0);
|
|
expect(span.attributes[A.outcome]).toBe("ok");
|
|
// A successful exec leaves the native span status unset (default OTel status).
|
|
expect(span.status).toBeNull();
|
|
expect(span.attributes[A.execCriticalPath]).toBe(true);
|
|
// The wall time is a real, finite, non-negative number.
|
|
expect(typeof span.attributes[A.execWallMs]).toBe("number");
|
|
expect(span.attributes[A.execWallMs] as number).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it("carries the explicit exec cache_hit from result.metadata.cacheHit", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: {
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "ok",
|
|
stderr: "",
|
|
metadata: { durationMs: 600, getDurationMs: 0, cacheHit: true },
|
|
},
|
|
tracer,
|
|
});
|
|
|
|
await runner.execute({ command: "echo", args: ["a"] });
|
|
|
|
const span = spans[0]!;
|
|
// The flag comes from the metadata boolean, not from a zero handle-fetch.
|
|
expect(span.attributes[A.execCacheHit]).toBe(true);
|
|
});
|
|
|
|
it("omits exec cache_hit when the provider reports no cacheHit metadata", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: {
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "ok",
|
|
stderr: "",
|
|
metadata: { durationMs: 600, getDurationMs: 15 },
|
|
},
|
|
tracer,
|
|
});
|
|
|
|
await runner.execute({ command: "echo", args: ["a"] });
|
|
|
|
const span = spans[0]!;
|
|
// A provider that omits the boolean yields no attribute — never `false`.
|
|
expect(span.attributes).not.toHaveProperty(A.execCacheHit);
|
|
});
|
|
|
|
it("omits each duration attribute when a provider returns no timing (does not throw, keeps provider)", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "kubernetes",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer,
|
|
});
|
|
|
|
await expect(runner.execute({ command: "echo" })).resolves.toBeTruthy();
|
|
|
|
expect(spans).toHaveLength(1);
|
|
const span = spans[0]!;
|
|
expect(A.execSandboxMs in span.attributes).toBe(false);
|
|
expect(A.execWaitBeforeMs in span.attributes).toBe(false);
|
|
// With no provider durations, the derived network time is also omitted.
|
|
expect(A.execNetworkMs in span.attributes).toBe(false);
|
|
// The provider attribute is always present so a trace shows which provider ran.
|
|
expect(span.attributes[A.provider]).toBe("kubernetes");
|
|
});
|
|
|
|
it("never emits a `0` duration attribute for a Daytona timeout that omits durationMs", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: {
|
|
exitCode: 124,
|
|
signal: null,
|
|
timedOut: true,
|
|
stdout: "",
|
|
stderr: "",
|
|
// The Daytona timeout branch may leave durationMs undefined.
|
|
metadata: { getDurationMs: 20 },
|
|
},
|
|
tracer,
|
|
});
|
|
|
|
await runner.execute({ command: "sleep", args: ["999"] });
|
|
|
|
const span = spans[0]!;
|
|
expect(A.execSandboxMs in span.attributes).toBe(false);
|
|
expect(span.attributes[A.execWaitBeforeMs]).toBe(20);
|
|
// A non-zero exit yields `failed`; the exit code rides as a number.
|
|
expect(span.attributes[A.outcome]).toBe("failed");
|
|
// A failed exec also sets the native span status to ERROR.
|
|
expect(span.status).toEqual({ code: SPAN_STATUS_CODE_ERROR });
|
|
expect(span.attributes[A.execExitCode]).toBe(124);
|
|
// `sleep` is not in the known-command allowlist, so it clamps to `other`.
|
|
expect(span.attributes[A.execCommand]).toBe("other");
|
|
});
|
|
|
|
it("never sets a command, arg, env, cwd, or stream text as a span attribute, even with secret-like input", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: {
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
// Secret-like standard-stream text must never ride the span.
|
|
stdout: "AKIAIOSFODNN7EXAMPLE token=s3cr3t-stdout",
|
|
stderr: "error at /home/agent/.ssh/id_rsa: s3cr3t-stderr",
|
|
metadata: { durationMs: 5, getDurationMs: 1 },
|
|
},
|
|
tracer,
|
|
});
|
|
|
|
await runner.execute({
|
|
// A full command line, a secret-like argument, a secret-like env value, a
|
|
// stdin blob, and a path-like cwd — none may ride the span.
|
|
command: "bash -lc 'rm -rf /secret/path'",
|
|
args: ["--token", "s3cr3t-arg", "--password", "hunter2"],
|
|
env: { AWS_SECRET_ACCESS_KEY: "s3cr3t-env", HOME: "/home/agent" },
|
|
cwd: "/home/agent/secret-workspace/.git",
|
|
stdin: "s3cr3t-stdin-blob",
|
|
});
|
|
|
|
const span = spans[0]!;
|
|
for (const key of Object.keys(span.attributes)) {
|
|
expect(ALLOWED_EXEC_SPAN_ATTRIBUTE_KEYS.has(key), `non-allowlisted key "${key}"`).toBe(true);
|
|
}
|
|
// The command clamps to the bounded `other` fallback (not a known basename).
|
|
expect(span.attributes[A.execCommand]).toBe("other");
|
|
// No forbidden substring rides any attribute value.
|
|
const values = Object.values(span.attributes).map(String);
|
|
for (const forbidden of [
|
|
"rm -rf",
|
|
"s3cr3t",
|
|
"hunter2",
|
|
"AKIA",
|
|
"id_rsa",
|
|
"/secret/path",
|
|
"/home/agent",
|
|
"secret-workspace",
|
|
"stdin",
|
|
]) {
|
|
expect(
|
|
values.some((value) => value.includes(forbidden)),
|
|
`attribute value leaked "${forbidden}"`,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("normalizes a plugin-backed provider key to `plugin` and keeps a built-in family as-is", async () => {
|
|
const plugin = createRecordingExecTracer();
|
|
const pluginRunner = await runnerFor({
|
|
provider: "acme-custom-sandbox",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer: plugin.tracer,
|
|
});
|
|
await pluginRunner.execute({ command: "echo" });
|
|
expect(plugin.spans[0]!.attributes[A.provider]).toBe("plugin");
|
|
|
|
const builtIn = createRecordingExecTracer();
|
|
const builtInRunner = await runnerFor({
|
|
provider: "e2b",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer: builtIn.tracer,
|
|
});
|
|
await builtInRunner.execute({ command: "echo" });
|
|
expect(builtIn.spans[0]!.attributes[A.provider]).toBe("e2b");
|
|
});
|
|
|
|
it("parents the exec span to the active step span when the exec runs inside a measured step", async () => {
|
|
const { tracer, contextWithSpan, spans } = createRecordingTrace();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer,
|
|
});
|
|
|
|
// The exec seam reads the active step context through `getActiveStepContext`.
|
|
// Wrap the exec in one measured step and assert the exec span parents to the
|
|
// step span, not to nothing.
|
|
await measureStartupStep({}, () => 0, "stage.sync", () => runner.execute({ command: "echo" }), {
|
|
tracer,
|
|
contextWithSpan,
|
|
});
|
|
|
|
const stepSpan = spans.find((span) => span.name === "stage.sync");
|
|
const execSpan = spans.find((span) => span.name === "sandbox.exec");
|
|
expect(stepSpan).toBeTruthy();
|
|
expect(execSpan).toBeTruthy();
|
|
expect(execSpan!.parent).toBe(stepSpan);
|
|
});
|
|
|
|
it("opens an unparented exec span when the exec runs outside any measured step", async () => {
|
|
const { tracer, spans } = createRecordingTrace();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer,
|
|
});
|
|
|
|
// No measured step wraps the exec, so the active step context is null and
|
|
// the exec span opens unparented (a no-op when tracing is off).
|
|
await runner.execute({ command: "echo" });
|
|
|
|
const execSpan = spans.find((span) => span.name === "sandbox.exec");
|
|
expect(execSpan).toBeTruthy();
|
|
expect(execSpan!.parent).toBeNull();
|
|
});
|
|
|
|
it("opens the exec span before the provider await so the span wraps the execution", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
// Assert the span is already open (started, not ended) while the provider
|
|
// runs. If the seam opened the span after the await, no open span would
|
|
// exist here and the native span duration would be near zero.
|
|
const runner = await runnerWithExecute({
|
|
provider: "daytona",
|
|
tracer,
|
|
execute: vi.fn().mockImplementation(async () => {
|
|
const open = spans.find((span) => span.name === "sandbox.exec");
|
|
expect(open, "the exec span must be open during the provider await").toBeTruthy();
|
|
expect(open!.ended).toBe(false);
|
|
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" };
|
|
}),
|
|
});
|
|
|
|
await runner.execute({ command: "echo" });
|
|
|
|
const span = spans.find((s) => s.name === "sandbox.exec");
|
|
expect(span!.ended).toBe(true);
|
|
expect(span!.attributes[A.outcome]).toBe("ok");
|
|
});
|
|
|
|
it("records a failed exec span and rethrows when the provider execution throws", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
const runner = await runnerWithExecute({
|
|
provider: "daytona",
|
|
tracer,
|
|
execute: vi.fn().mockRejectedValue(new Error("provider transport failed")),
|
|
});
|
|
|
|
// The original error rides through unchanged; observability never swallows it.
|
|
await expect(runner.execute({ command: "echo" })).rejects.toThrow("provider transport failed");
|
|
|
|
// A thrown execution still produces one ended span with the `failed` outcome,
|
|
// instead of no span at all.
|
|
const span = spans.find((s) => s.name === "sandbox.exec");
|
|
expect(span).toBeTruthy();
|
|
expect(span!.ended).toBe(true);
|
|
expect(span!.attributes[A.outcome]).toBe("failed");
|
|
// A thrown execution also sets the native span status to ERROR.
|
|
expect(span!.status).toEqual({ code: SPAN_STATUS_CODE_ERROR });
|
|
expect(span!.attributes[A.provider]).toBe("daytona");
|
|
// `echo` is a known basename, so the clamped command rides the span.
|
|
expect(span!.attributes[A.execCommand]).toBe("echo");
|
|
// No exec result exists, so the exit code never rides the span.
|
|
expect(A.execExitCode in span!.attributes).toBe(false);
|
|
// The wall time is a real, finite, non-negative number.
|
|
expect(typeof span!.attributes[A.execWallMs]).toBe("number");
|
|
expect(span!.attributes[A.execWallMs] as number).toBeGreaterThanOrEqual(0);
|
|
// Only allowlisted keys ride the failed span.
|
|
for (const key of Object.keys(span!.attributes)) {
|
|
expect(ALLOWED_EXEC_SPAN_ATTRIBUTE_KEYS.has(key), `non-allowlisted key "${key}"`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps the exec span outcome `ok` when the execution succeeds but a log callback rejects", async () => {
|
|
const { tracer, spans } = createRecordingExecTracer();
|
|
// The provider execution succeeds and returns stdout, so the seam invokes
|
|
// the log callback. The callback rejects, which models a downstream log-sink
|
|
// failure. The rejection must reach the caller, but it must never reclassify
|
|
// the successful execution as a failed span.
|
|
const runner = await runnerWithExecute({
|
|
provider: "daytona",
|
|
tracer,
|
|
execute: vi.fn().mockResolvedValue({
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "hello",
|
|
stderr: "",
|
|
}),
|
|
});
|
|
|
|
const onLog = vi.fn().mockRejectedValue(new Error("log sink rejected"));
|
|
// The rejection rides through unchanged; observability never swallows it.
|
|
await expect(
|
|
(runner as { execute(input: unknown): Promise<unknown> }).execute({ command: "echo", onLog }),
|
|
).rejects.toThrow("log sink rejected");
|
|
|
|
// The span ended with the successful outcome from the command result, not
|
|
// the failed outcome. A log failure never marks the execution failed.
|
|
const span = spans.find((s) => s.name === "sandbox.exec");
|
|
expect(span).toBeTruthy();
|
|
expect(span!.ended).toBe(true);
|
|
expect(span!.attributes[A.outcome]).toBe("ok");
|
|
// A log-callback rejection never marks the span as ERROR; the status stays unset.
|
|
expect(span!.status).toBeNull();
|
|
expect(span!.attributes[A.execExitCode]).toBe(0);
|
|
// The seam reached the log callback exactly once (the stdout delivery).
|
|
expect(onLog).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
// Fire one run-time exec from a bridge continuation that runs after the step
|
|
// span ended. Each bridge step (`bridge.paperclip`, `bridge.process-session`)
|
|
// starts long-lived work with `criticalPath: false`. The bridge boundary wraps
|
|
// that long-lived work in `runWithoutActiveStep`, exactly as modeled here, so
|
|
// the continuation reads an empty active step. Return the recorded exec span.
|
|
async function runContinuationExec(step: string, options: { wrap: boolean }) {
|
|
const { tracer, contextWithSpan, spans } = createRecordingTrace();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer,
|
|
});
|
|
|
|
let resolveExec!: () => void;
|
|
const execDone = new Promise<void>((resolve) => {
|
|
resolveExec = resolve;
|
|
});
|
|
|
|
// Schedule the exec from a timer inside the step body, so it fires after the
|
|
// step span ends. The `wrap` flag models the fix: when true, the boundary
|
|
// wraps the long-lived work in `runWithoutActiveStep`; when false, it models
|
|
// the pre-fix leak.
|
|
const scheduleContinuation = () => {
|
|
setTimeout(() => {
|
|
void runner.execute({ command: "echo" }).then(() => resolveExec());
|
|
}, 0);
|
|
};
|
|
|
|
await measureStartupStep(
|
|
{},
|
|
() => 0,
|
|
step,
|
|
async () => {
|
|
if (options.wrap) {
|
|
runWithoutActiveStep(scheduleContinuation);
|
|
} else {
|
|
scheduleContinuation();
|
|
}
|
|
return "started";
|
|
},
|
|
{ tracer, contextWithSpan, criticalPath: false },
|
|
);
|
|
|
|
await execDone;
|
|
return spans.find((span) => span.name === "sandbox.exec");
|
|
}
|
|
|
|
it("opens an unparented exec span for a process-session bridge continuation", async () => {
|
|
const execSpan = await runContinuationExec("bridge.process-session", { wrap: true });
|
|
expect(execSpan).toBeTruthy();
|
|
// The step span ended and the boundary emptied the store, so the continuation
|
|
// exec opens a root span, not one under the dead bridge step.
|
|
expect(execSpan!.parent).toBeNull();
|
|
});
|
|
|
|
it("opens an unparented exec span for a paperclip bridge continuation", async () => {
|
|
const execSpan = await runContinuationExec("bridge.paperclip", { wrap: true });
|
|
expect(execSpan).toBeTruthy();
|
|
expect(execSpan!.parent).toBeNull();
|
|
});
|
|
|
|
it("does not copy the stale criticalPath = false flag onto a continuation exec", async () => {
|
|
const execSpan = await runContinuationExec("bridge.process-session", { wrap: true });
|
|
expect(execSpan).toBeTruthy();
|
|
// The bridge step set `criticalPath: false`. The continuation reads an empty
|
|
// store, so the exec span records the default `true`, never the stale `false`.
|
|
expect(execSpan!.attributes[A.execCriticalPath]).toBe(true);
|
|
expect(execSpan!.attributes[A.execCriticalPath]).not.toBe(false);
|
|
});
|
|
|
|
it("leaks the ended step onto a continuation exec without the boundary wrap", async () => {
|
|
// The mechanism guard: an unwrapped continuation keeps the ended bridge step
|
|
// store, so the exec span parents to the dead step and copies its
|
|
// `criticalPath: false`. The boundary wrap in the two tests above removes both
|
|
// defects, so this suite fails if a future edit drops the wrap.
|
|
const { tracer, contextWithSpan, spans } = createRecordingTrace();
|
|
const runner = await runnerFor({
|
|
provider: "daytona",
|
|
execResult: { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" },
|
|
tracer,
|
|
});
|
|
|
|
let resolveExec!: () => void;
|
|
const execDone = new Promise<void>((resolve) => {
|
|
resolveExec = resolve;
|
|
});
|
|
|
|
await measureStartupStep(
|
|
{},
|
|
() => 0,
|
|
"bridge.process-session",
|
|
async () => {
|
|
setTimeout(() => {
|
|
void runner.execute({ command: "echo" }).then(() => resolveExec());
|
|
}, 0);
|
|
return "started";
|
|
},
|
|
{ tracer, contextWithSpan, criticalPath: false },
|
|
);
|
|
|
|
await execDone;
|
|
|
|
const stepSpan = spans.find((span) => span.name === "bridge.process-session");
|
|
const execSpan = spans.find((span) => span.name === "sandbox.exec");
|
|
expect(stepSpan).toBeTruthy();
|
|
expect(execSpan).toBeTruthy();
|
|
// The unwrapped continuation parents the exec span to the ended step and
|
|
// copies the stale flag.
|
|
expect(execSpan!.parent).toBe(stepSpan);
|
|
expect(execSpan!.attributes[A.execCriticalPath]).toBe(false);
|
|
});
|
|
});
|