mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
## Thinking Path > - Paperclip manages AI agents and their work. > - Native agents need verified provider files in remote sandboxes. > - A fresh sandbox can need the bundled provider-pack archive. > - The upload path also hashes the expanded controller tree, which it does not upload. > - That synchronous scan blocks the server event loop and delays other requests. > - This change verifies the archive source and the installed pack, and keeps source-tree checks for directory uploads. ## Linked Issues or Issue Description **What happened?** Provider-pack archive fallback scans and hashes the full expanded controller tree before reading the build archive. The archive is the source sent to the sandbox. The extra scan blocks the Node event loop for several seconds. An overlapping asynchronous operation can then appear to take as long as that scan. **Expected behavior** Verify the bytes used by the selected upload path. Keep full verification of the installed pack against the selected manifest before a provider can use it. **Steps to reproduce** 1. Use a packaged provider pack with a build archive. 2. Start a remote native run whose sandbox needs that pack. 3. Inspect `provider_pack.archive.verify` and event-loop delay on the controller. Related: #18308 adds timing detail for workspace preparation. A search found no duplicate archive-verification fix. ## What Changed - Use the verified archive without scanning the unused expanded directory. - Keep full directory verification when no archive exists or the remote lacks archive support. - Keep archive digest checks and full verification of installed bytes. - Add `provider_pack.directory.verify` for directory fallback. - Add regression cases for both archive formats, fallback, and final verification failure. ## Verification - Both archive regressions failed before the change and passed after it. - 80 focused archive, selection, and verification cases pass on the rebased branch. - 20 native manifest and admission cases pass. - Server typecheck, server build, and the runtime bundle pass. - A disposable sandbox from the recorded image exercised the real uploader and full installed-pack verifier. It accepted the unchanged build bytes and rejected an altered proxy file. No model was called. The sandbox was deleted and its absence was verified. - A local baseline/archive/archive/baseline probe used the same unchanged public build pack. Preparation took 30.673 s, 84 ms, 106 ms, and 5.371 s. The slow baseline outlier is retained. This measures one controller phase; it does not establish a whole-turn or hosted speedup. ## Risks The archive path no longer checks expanded files that it does not send. Archive checks still fail closed, and the installed files must pass the same full manifest verification. Directory fallback keeps its source-tree check. Installs without archives retain that synchronous scan. The local sandbox probe tests upload and installed-content checks; it does not qualify admission of an older pack by a newer controller. ## Model Used OpenAI Codex, GPT-6 family, with tool use and local code execution. The exact runtime model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
45 lines
2.2 KiB
TypeScript
45 lines
2.2 KiB
TypeScript
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";
|
|
import type { NativeRunTrace } from "./native-run-trace.js";
|
|
import { readProviderPackArchive, stageProviderPackArchive } from "./provider-pack-archive.js";
|
|
|
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
|
|
/** Upload a pack and verify the installed bytes before exposing it to a provider. */
|
|
export async function uploadVerifiedProviderPack(input: {
|
|
root: string;
|
|
expectedDigest: string;
|
|
runner: CommandManagedRuntimeRunner;
|
|
cwd: string;
|
|
targetPath: string;
|
|
reuseRoots: readonly string[];
|
|
trace?: NativeRunTrace;
|
|
verifyDirectory: () => void;
|
|
stageDirectory: () => Promise<void>;
|
|
verifyUploaded: () => Promise<void>;
|
|
}): Promise<void> {
|
|
const measure = <T>(name: string, fn: () => Promise<T>) => input.trace ? input.trace.measure(name, fn) : fn();
|
|
const verifyDirectory = () => measure("provider_pack.directory.verify", async () => input.verifyDirectory());
|
|
// Archive installation reads the sealed archives, not the expanded tree.
|
|
// Hash the directory only when it is actually the upload source. The
|
|
// installed pack still passes full verification against the selected identity.
|
|
const archive = await measure("provider_pack.archive.verify", () =>
|
|
readProviderPackArchive(input.root, input.expectedDigest));
|
|
if (!archive) await verifyDirectory();
|
|
const cleared = await measure("provider_pack.clear_stale", () => input.runner.execute({
|
|
command: "sh", args: ["-c", `rm -rf ${quote(input.targetPath)}`],
|
|
cwd: input.cwd, bypassSession: true, timeoutMs: 10_000,
|
|
}));
|
|
if (cleared.exitCode !== 0 || cleared.timedOut) {
|
|
throw new Error("runner_remote_provider_artifact_incompatible: stale provider pack could not be replaced");
|
|
}
|
|
await measure("provider_pack.upload", async () => {
|
|
if (archive && await stageProviderPackArchive({
|
|
archive, runner: input.runner, targetPath: input.targetPath,
|
|
reuseRoots: input.reuseRoots, trace: input.trace,
|
|
})) return;
|
|
if (archive) await verifyDirectory();
|
|
await input.stageDirectory();
|
|
});
|
|
await measure("provider_pack.verify_uploaded", input.verifyUploaded);
|
|
}
|