mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
## Thinking Path > - Paperclip is the control plane people use to coordinate AI agents and their execution environments. > - Environment realization decides where an agent runs and which filesystem and toolchain are authoritative. > - Copy-based realization is unsafe for container-anchored tasks because absolute paths such as `/app` can point outside the synchronized tree and task-specific binaries may be absent. > - That mismatch can let an agent successfully verify work in a phantom writable path while sync-back silently discards the result. > - Existing task environments already provide the authoritative filesystem and toolchain, so they should be executed in place rather than copied. > - Copy mode still needs explicit confinement rules so aliases target the synchronized workspace and unsynchronized writable paths fail visibly. > - This pull request adds typed realization metadata, propagates the authoritative root through orchestration, and teaches Codex to honor it. > - The benefit is that container-anchored tasks operate on verifier-visible state with the intended tools, while copy mode remains safe and backward compatible. ## Linked Issues or Issue Description No public GitHub issue exists for this defect. GitHub duplicate searches for in-place execution, workspace realization, and authoritative workspace roots found no related pull request to link. ### What happened? Environment-backed agent runs were always realized through a copied workspace. Tasks anchored to absolute container paths could therefore write outside the synchronized tree, and task-provided toolchains were unavailable in the copy. A run could report success even though sync-back discarded its output. ### Expected behavior Existing task environments should run against their real authoritative root and toolchain. Copy-mode runs should map declared absolute aliases into the synchronized tree and reject writable paths that cannot be restored. ### Steps to reproduce 1. Run a Codex task environment whose required files live under `/app` or `/workspace` and whose required binary exists only in the task container. 2. Observe that copy realization changes the effective filesystem/toolchain or permits writes outside the synchronized root. 3. Complete and verify the task inside the agent sandbox. 4. Observe that the verifier cannot see out-of-tree artifacts or that task-specific commands were unavailable. ### Reproduction context - Paperclip commit: `3a16b91217483d2c233926de5b7f7bc3a1077924` - Deployment: built from source in a task-container execution environment - Adapter: Codex local - Database: not database-related - Access context: agent execution ## What Changed - Added typed `copy | in_place` workspace-realization metadata, authoritative roots, confined aliases, and outbound restore paths to shared execution-target contracts. - Selected in-place realization for existing task environments and skipped archive prepare/restore when the authoritative environment is used directly. - Propagated the authoritative root into adapter context so Codex uses it for cwd and `PAPERCLIP_WORKSPACE_*` semantics, including ACP execution. - Bound copy-mode aliases such as `/app` to the synchronized workspace and rejected writable out-of-tree paths without explicit restore mappings. - Added focused regression coverage while preserving existing copy-mode archive restore behavior. ## Verification - `pnpm --filter @paperclipai/shared typecheck` — passed. - `pnpm exec vitest run packages/adapter-utils/src/local-process-sandbox.test.ts packages/adapters/codex-local/src/server/acp.test.ts packages/adapters/codex-local/src/server/execute.remote.test.ts server/src/__tests__/environment-run-orchestrator.test.ts` — 48 passed, 4 skipped. - `pnpm -r typecheck` — passed. - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN pnpm test:run` — passed across all general and serialized Vitest shards. - `pnpm build` — passed. - Codex `k=1` acceptance run completed July 24, 2026 at 23:54:30 UTC with 4 completed, 0 exceptions, and mean reward 1.0: `build-cython-ext`, `openssl-selfsigned-cert`, `prove-plus-comm`, and `sqlite-db-truncate` each received terminal grade 1.0 against real task-environment paths and toolchains. ## Risks - In-place mode deliberately exposes the authoritative task root to the adapter; incorrect environment metadata could point execution at the wrong root. Typed metadata and focused orchestration tests cover selection and propagation. - Copy-mode writable-path validation is stricter and may reject previously accepted unsafe configurations. The rejection is intentional and produces a visible error instead of silently losing output. - The acceptance run is focused on four Codex task-environment workloads, not a broad cross-adapter benchmark. Existing copy-mode archive tests and the full repository suite remain green. > 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 Codex CLI coding agent; exact model ID and context-window size were not exposed to this runtime. Capabilities used: extended reasoning, repository editing, shell execution, test/build execution, Git, GitHub CLI, and Paperclip API tool use. ## 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>
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
import type {
|
|
Environment,
|
|
EnvironmentLease,
|
|
ExecutionWorkspaceConfig,
|
|
WorkspaceRealizationRecord,
|
|
WorkspaceRealizationRequest,
|
|
} from "@paperclipai/shared";
|
|
import type { RealizedExecutionWorkspace } from "./workspace-runtime.js";
|
|
|
|
function parseObject(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
function readString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
}
|
|
|
|
function readNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function readStringArray(value: unknown): string[] {
|
|
return Array.isArray(value)
|
|
? value.map(readString).filter((entry): entry is string => entry !== null)
|
|
: [];
|
|
}
|
|
|
|
function readPathAliases(value: unknown): Array<{ path: string; target: string }> {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.flatMap((entry) => {
|
|
const parsed = parseObject(entry);
|
|
const aliasPath = readString(parsed.path);
|
|
const target = readString(parsed.target);
|
|
return aliasPath && target ? [{ path: aliasPath, target }] : [];
|
|
});
|
|
}
|
|
|
|
function readWorkspaceRealizationRequest(value: unknown): WorkspaceRealizationRequest | null {
|
|
const parsed = parseObject(value);
|
|
if (parsed.version !== 1) return null;
|
|
const source = parseObject(parsed.source);
|
|
const runtimeOverlay = parseObject(parsed.runtimeOverlay);
|
|
const localPath = readString(source.localPath);
|
|
const companyId = readString(parsed.companyId);
|
|
const environmentId = readString(parsed.environmentId);
|
|
const heartbeatRunId = readString(parsed.heartbeatRunId);
|
|
const adapterType = readString(parsed.adapterType);
|
|
if (!localPath || !companyId || !environmentId || !heartbeatRunId || !adapterType) return null;
|
|
|
|
return {
|
|
version: 1,
|
|
adapterType,
|
|
companyId,
|
|
environmentId,
|
|
executionWorkspaceId: readString(parsed.executionWorkspaceId),
|
|
issueId: readString(parsed.issueId),
|
|
heartbeatRunId,
|
|
requestedMode: readString(parsed.requestedMode),
|
|
source: {
|
|
kind:
|
|
source.kind === "task_session" || source.kind === "agent_home"
|
|
? source.kind
|
|
: "project_primary",
|
|
localPath,
|
|
projectId: readString(source.projectId),
|
|
projectWorkspaceId: readString(source.projectWorkspaceId),
|
|
repoUrl: readString(source.repoUrl),
|
|
repoRef: readString(source.repoRef),
|
|
strategy: source.strategy === "git_worktree" ? "git_worktree" : "project_primary",
|
|
branchName: readString(source.branchName),
|
|
worktreePath: readString(source.worktreePath),
|
|
},
|
|
runtimeOverlay: {
|
|
provisionCommand: readString(runtimeOverlay.provisionCommand),
|
|
teardownCommand: readString(runtimeOverlay.teardownCommand),
|
|
cleanupCommand: readString(runtimeOverlay.cleanupCommand),
|
|
workspaceRuntime: Object.keys(parseObject(runtimeOverlay.workspaceRuntime)).length > 0
|
|
? parseObject(runtimeOverlay.workspaceRuntime)
|
|
: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildWorkspaceRealizationRequest(input: {
|
|
adapterType: string;
|
|
companyId: string;
|
|
environmentId: string;
|
|
executionWorkspaceId: string | null;
|
|
issueId: string | null;
|
|
heartbeatRunId: string;
|
|
requestedMode: string | null;
|
|
workspace: RealizedExecutionWorkspace;
|
|
workspaceConfig: ExecutionWorkspaceConfig | null;
|
|
}): WorkspaceRealizationRequest {
|
|
return {
|
|
version: 1,
|
|
adapterType: input.adapterType,
|
|
companyId: input.companyId,
|
|
environmentId: input.environmentId,
|
|
executionWorkspaceId: input.executionWorkspaceId,
|
|
issueId: input.issueId,
|
|
heartbeatRunId: input.heartbeatRunId,
|
|
requestedMode: input.requestedMode,
|
|
source: {
|
|
kind: input.workspace.source,
|
|
localPath: input.workspace.cwd,
|
|
projectId: input.workspace.projectId,
|
|
projectWorkspaceId: input.workspace.workspaceId,
|
|
repoUrl: input.workspace.repoUrl,
|
|
repoRef: input.workspace.repoRef,
|
|
strategy: input.workspace.strategy,
|
|
branchName: input.workspace.branchName,
|
|
worktreePath: input.workspace.worktreePath,
|
|
},
|
|
runtimeOverlay: {
|
|
provisionCommand: input.workspaceConfig?.provisionCommand ?? null,
|
|
teardownCommand: input.workspaceConfig?.teardownCommand ?? null,
|
|
cleanupCommand: input.workspaceConfig?.cleanupCommand ?? null,
|
|
workspaceRuntime: input.workspaceConfig?.workspaceRuntime ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildWorkspaceRealizationRecord(input: {
|
|
environment: Environment;
|
|
lease: EnvironmentLease;
|
|
request: WorkspaceRealizationRequest;
|
|
realizedCwd?: string | null;
|
|
providerMetadata?: Record<string, unknown> | null;
|
|
}): WorkspaceRealizationRecord {
|
|
const leaseMetadata = input.lease.metadata ?? {};
|
|
const providerMetadata = input.providerMetadata ?? {};
|
|
const transport =
|
|
input.environment.driver === "ssh" || input.environment.driver === "sandbox" || input.environment.driver === "plugin"
|
|
? input.environment.driver
|
|
: "local";
|
|
const remotePath =
|
|
readString(providerMetadata.remoteCwd) ??
|
|
readString(leaseMetadata.remoteCwd) ??
|
|
readString(providerMetadata.remotePath) ??
|
|
null;
|
|
const host = readString(leaseMetadata.host);
|
|
const port = readNumber(leaseMetadata.port);
|
|
const username = readString(leaseMetadata.username);
|
|
const sandboxId = readString(leaseMetadata.sandboxId) ?? readString(providerMetadata.sandboxId);
|
|
const realizationMetadata = {
|
|
...parseObject(leaseMetadata.workspaceRealization),
|
|
...parseObject(providerMetadata.workspaceRealization),
|
|
...providerMetadata,
|
|
};
|
|
const mode = realizationMetadata.mode === "in_place" || realizationMetadata.realizationMode === "in_place"
|
|
? "in_place" as const
|
|
: "copy" as const;
|
|
const authoritativeRoot =
|
|
readString(realizationMetadata.authoritativeRoot) ??
|
|
(mode === "in_place" ? remotePath : null) ??
|
|
input.request.source.localPath;
|
|
const pathAliases = readPathAliases(realizationMetadata.pathAliases ?? realizationMetadata.workspaceAliases);
|
|
const outboundRestorePaths = readStringArray(realizationMetadata.outboundRestorePaths);
|
|
|
|
const sync = (() => {
|
|
if (mode === "in_place" || transport === "local") {
|
|
return {
|
|
strategy: "none" as const,
|
|
prepare: "Use the realized local execution workspace directly.",
|
|
syncBack: null,
|
|
};
|
|
}
|
|
if (transport === "ssh") {
|
|
return {
|
|
strategy: "ssh_git_import_export" as const,
|
|
prepare: "Import the local git workspace to the remote SSH workspace before adapter execution.",
|
|
syncBack: "Export remote SSH workspace changes back to the local execution workspace after adapter execution.",
|
|
};
|
|
}
|
|
if (transport === "sandbox") {
|
|
return {
|
|
strategy: "sandbox_archive_upload_download" as const,
|
|
prepare: "Upload a workspace archive into the sandbox filesystem before adapter execution.",
|
|
syncBack: "Download a workspace archive from the sandbox and mirror it back locally after adapter execution.",
|
|
};
|
|
}
|
|
return {
|
|
strategy: "provider_defined" as const,
|
|
prepare: "Delegate workspace materialization to the plugin environment driver.",
|
|
syncBack: "Delegate result synchronization to the plugin environment driver.",
|
|
};
|
|
})();
|
|
|
|
const provider =
|
|
input.lease.provider ??
|
|
(transport === "ssh" ? "ssh" : transport === "local" ? "local" : null);
|
|
const localPath = input.request.source.localPath;
|
|
const summary =
|
|
transport === "local"
|
|
? `Local workspace realized at ${localPath}.`
|
|
: transport === "ssh"
|
|
? `SSH workspace realized at ${username ?? "user"}@${host ?? "host"}:${port ?? 22}:${remotePath ?? input.request.source.localPath}.`
|
|
: transport === "sandbox"
|
|
? `Sandbox workspace realized at ${remotePath ?? "/"}${sandboxId ? ` in ${sandboxId}` : ""}.`
|
|
: `Plugin workspace realized at ${input.realizedCwd ?? remotePath ?? localPath}.`;
|
|
|
|
return {
|
|
version: 1,
|
|
mode,
|
|
authoritativeRoot,
|
|
pathAliases,
|
|
outboundRestorePaths,
|
|
transport,
|
|
provider,
|
|
environmentId: input.environment.id,
|
|
leaseId: input.lease.id,
|
|
providerLeaseId: input.lease.providerLeaseId,
|
|
local: {
|
|
path: localPath,
|
|
source: input.request.source.kind,
|
|
strategy: input.request.source.strategy,
|
|
projectId: input.request.source.projectId,
|
|
projectWorkspaceId: input.request.source.projectWorkspaceId,
|
|
repoUrl: input.request.source.repoUrl,
|
|
repoRef: input.request.source.repoRef,
|
|
branchName: input.request.source.branchName,
|
|
worktreePath: input.request.source.worktreePath,
|
|
},
|
|
remote: {
|
|
path: remotePath,
|
|
...(host ? { host } : {}),
|
|
...(port ? { port } : {}),
|
|
...(username ? { username } : {}),
|
|
...(sandboxId ? { sandboxId } : {}),
|
|
},
|
|
sync,
|
|
bootstrap: {
|
|
command: input.request.runtimeOverlay.provisionCommand,
|
|
},
|
|
rebuild: {
|
|
executionWorkspaceId: input.request.executionWorkspaceId,
|
|
mode: input.request.requestedMode,
|
|
repoUrl: input.request.source.repoUrl,
|
|
repoRef: input.request.source.repoRef,
|
|
localPath,
|
|
remotePath,
|
|
providerLeaseId: input.lease.providerLeaseId,
|
|
metadata: {
|
|
source: input.request.source,
|
|
runtimeOverlay: input.request.runtimeOverlay,
|
|
environmentDriver: input.environment.driver,
|
|
provider,
|
|
providerMetadata,
|
|
},
|
|
},
|
|
summary,
|
|
};
|
|
}
|
|
|
|
export function buildWorkspaceRealizationRecordFromDriverInput(input: {
|
|
environment: Environment;
|
|
lease: EnvironmentLease;
|
|
workspace: {
|
|
localPath?: string;
|
|
remotePath?: string;
|
|
mode?: string;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
cwd?: string | null;
|
|
providerMetadata?: Record<string, unknown> | null;
|
|
}): WorkspaceRealizationRecord {
|
|
const request =
|
|
readWorkspaceRealizationRequest(input.workspace.metadata?.workspaceRealizationRequest) ??
|
|
readWorkspaceRealizationRequest(input.workspace.metadata?.request) ??
|
|
buildWorkspaceRealizationRequest({
|
|
adapterType: "unknown",
|
|
companyId: input.lease.companyId,
|
|
environmentId: input.environment.id,
|
|
executionWorkspaceId: input.lease.executionWorkspaceId,
|
|
issueId: input.lease.issueId,
|
|
heartbeatRunId: input.lease.heartbeatRunId ?? "unknown",
|
|
requestedMode: input.workspace.mode ?? null,
|
|
workspace: {
|
|
baseCwd: input.workspace.localPath ?? input.cwd ?? input.workspace.remotePath ?? "/",
|
|
source: "task_session",
|
|
projectId: null,
|
|
workspaceId: null,
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
strategy: "project_primary",
|
|
cwd: input.workspace.localPath ?? input.cwd ?? input.workspace.remotePath ?? "/",
|
|
branchName: null,
|
|
worktreePath: null,
|
|
warnings: [],
|
|
created: false,
|
|
},
|
|
workspaceConfig: null,
|
|
});
|
|
|
|
return buildWorkspaceRealizationRecord({
|
|
environment: input.environment,
|
|
lease: input.lease,
|
|
request,
|
|
realizedCwd: input.cwd ?? null,
|
|
providerMetadata: input.providerMetadata,
|
|
});
|
|
}
|