mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Remote native runs use a provider pack. The controller reuses a protected pack from the sandbox image when the pack is compatible. > - #17749 gates this reuse on a hand-maintained interface revision in `provider-pack-contract.json`. > - Later changes altered the runner/pack boundary (#17922, #18188), but nobody bumped the revision. A stale Claude or Codex image pack can still be reused across a sidecar contract change. This is the failure class of #17972. > - A manual revision has a "forgot to bump" failure mode. Source-byte hashes change on every refactor, so they are too brittle. > - This pull request declares the sidecar `session.open` contract in its JSON schema and computes a contract digest from the JSON contracts and Rust shapes. > - Image-pack reuse now requires an equal digest. > - The benefit is that an incidental change keeps image packs reusable, and a real contract change retires them automatically. ## Linked Issues or Issue Description Refs #17749, #17972, #17922, #18188. **What happened?** The controller reused an image provider pack when `compatibility.acpx` and the selected agent's profile digest matched. For Claude and Codex, that profile digest binds only the upstream executable. The ACPX interface revision stayed at 1 after later sidecar contract changes. A pack built before those changes passed the reuse check. **Expected behavior** The controller reuses an image pack only when the pack uses the same runner/pack contracts as the controller's bundled pack. Comments, formatting and refactors do not change that result. **Steps to reproduce** 1. Build an image pack from a commit before a sidecar input change, such as a new `session.open` field. 2. Run a controller from a later commit that sends the new field, with the same interface revision and Claude version. 3. Before this fix, `verifyRemoteProviderPack` accepts the old image pack. ## What Changed - Declare the sidecar `session.open` parameter set as a closed `sessionOpenParams` definition in `acpx-sidecar.schema.json`. - `generate-acpx-sidecar-contract.mjs` now generates `GENERATED_ACPX_SIDECAR_OPEN_PARAMS` from it. - `parseOpenParams` uses the generated list instead of an inline `Set`. The accepted set is unchanged. - This is the surface of #17972. The schema is now its source of truth. - Declare the nested `session.open` contracts that the sidecar parses strictly: - `protocol/provider-schemas/runtime-context.schema.json` closes every runtime context object. `parseNativeRuntimeContext` uses the field lists that `scripts/generate-runtime-context-contract.mjs` generates from it. - The sidecar schema declares the closed `providerPolicy` object, and `parseProviderPolicy` uses its generated field list. - The accepted fields do not change. - Add `packages/paperclip-runner/scripts/contracts/contract-fingerprints.mjs`. It fingerprints these contracts: - the PRP and sidecar JSON schemas, with annotation text removed - the generated catalogs in `generated/` - the Rust serde shapes, `json!` payload keys, match literals and env keys in `runner-core/src`, without test-only modules - the reserved env key lists in `configured_environment.rs` - Read only the trees that every pack build context contains. Fail when a registered source is missing, so two build contexts cannot silently produce different digests. - Use only Node built-ins. The registry adds no dependency. - Write `contractDigest` as a top-level payload field in `build-provider-pack.mjs`. The manifest digest algorithm and the existing field layout do not change. - Require an equal `contractDigest` for image-pack reuse in `remote-provider-pack-verification.ts`. A pack without a digest is not reused. The controller uploads its bundled pack, as it does today for any incompatible pack. - Re-attest the source-hashed profiles, because this PR changes their bound sidecar sources: - Grok becomes profile 15 and now also binds the generated allowlist, so a Grok field change in the schema still invalidates the Grok identity. - Copilot becomes profile 88, and also binds the generated runtime context fields. - Earlier profiles stay decodable. - Add the registry script and `generated/` to the Daytona image content inputs. - Document the digest and the schema-first rule in `docker/daytona-runner/README.md` and `doc/DEVELOPING.md`. ## Verification - `node --test scripts/contracts/contract-fingerprints.test.mjs` (in `packages/paperclip-runner`): 4 tests pass. - These changes keep the digest: - schema description text - schema reformatting - renaming `parseOpenParams` - Rust comments, `#[serde(skip)]` fields and `#[cfg(test)]` modules - edits to the `#[path]` test module `tests/provider_api_validation.rs` - These changes alter the digest: - a new `session.open` schema field - a changed constraint on the `remainingWork[].description` property - a new reserved env key - a new sidecar env key - a new checkpoint field - A build context without a registered source fails. - `node --test test/acpx-sidecar-contract.test.mjs`: 13 tests pass. A new case rejects an unknown `session.open` parameter and accepts each declared parameter. - `node --test scripts/grok-profile-identity.test.mjs`: 4 tests pass. A change to the Grok field in the generated allowlist invalidates the Grok identity. - `generate-acpx-profiles.mjs --check`, `generate-acpx-sidecar-contract.mjs --check` and `generate-protocol-manifest.mjs --check` pass. - Runner vitest suites pass: 470 passed, 1 skipped. They cover the sidecar, sidecar input and protocol, Copilot and Pi installation, profile compatibility, native execution, recovery identity, the backend factory and the environment. - Server tests: pack verification, executor and artifact preparation pass (1,004 tests). - `tsc --noEmit` passes for `packages/paperclip-runner` and `server`. - `tests/runner-e2e/daytona-image.test.ts`: 10 of 11 tests pass. The failing test also fails on `master`. It expects a literal `profile-v19-identity.json` line in `.dockerignore`, which uses a `profile-v*` glob. - Not run locally: - A complete `build-provider-pack.mjs` run. The builder rejects the local Homebrew Node because it is not portable after relocation. - Rust tests and `acpx-controller-recovery.test.ts`, because `cargo` is not installed. - The full `pnpm -r typecheck`, `pnpm test:run` and `pnpm build`. ## Risks - Only JSON contracts and Rust shapes are part of the digest. The remaining TypeScript-only definitions are the `paperclip_finish` and `paperclip_block` tool schemas in `completion-result.ts` and the TypeScript env key lists. They are not covered until they move into a schema. The nested `expectedIdentity` and `tools` values ignore unknown fields, so a change there cannot cause a startup rejection. - A change there can let a stale image pack be reused. The sidecar then rejects the input with its strict parser error. - Today, Claude and Codex image packs have no contract check at all. - The digest covers all registered contracts for the whole pack. A contract change for one provider kind also retires image packs for the other. Each replacement costs about 16 s per remote start until the image is refreshed. - Existing image packs have no digest. After deploy, the controller uploads its bundled pack until the image is rebuilt. - The Grok and Copilot re-attestations start fresh sessions for those agents once after deploy, as each profile bump does today. - The Daytona image content ID formula changes, so the next image build does not reuse a cached image. > 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 Anthropic Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code in the desktop app, with repository tools, shell execution and research subagents. The session does not expose the context window size. ## 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 - [ ] 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
355 lines
14 KiB
TypeScript
355 lines
14 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { lstat, readdir, readFile, readlink } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import acpxProfiles from "../../packages/paperclip-runner/acpx-profiles.json" with { type: "json" };
|
|
|
|
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
|
|
|
export const DAYTONA_IMAGE_CONTENT_SCHEMA =
|
|
"paperclip-daytona-runner-image-content/v5";
|
|
export const DAYTONA_IMAGE_PLATFORM = "linux/amd64";
|
|
export const DAYTONA_IMAGE_DOCKERFILE_PATH = "docker/daytona-runner/Dockerfile";
|
|
|
|
export const DAYTONA_IMAGE_INPUT_PATHS = [...new Set([
|
|
".dockerignore",
|
|
".npmrc",
|
|
"docker/daytona-runner/Dockerfile",
|
|
"package.json",
|
|
"patches",
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
"scripts/link-plugin-dev-sdk.mjs",
|
|
"tsconfig.base.json",
|
|
"packages/paperclip-eval-kernel/package.json",
|
|
"packages/paperclip-eval-kernel/src",
|
|
"packages/paperclip-eval-kernel/tsconfig.json",
|
|
"packages/paperclip-runner/scripts/provision-grok.mjs",
|
|
"packages/paperclip-runner/package.json",
|
|
"packages/paperclip-runner/acpx-profiles.json",
|
|
`packages/paperclip-runner/test/fixtures/copilot-profile-v${acpxProfiles.profiles.copilot.agentProfileVersion}-identity.json`,
|
|
"packages/paperclip-runner/cursor-distributions.json",
|
|
"packages/paperclip-runner/cursor-contract.json",
|
|
// The pack's contract digest fingerprints these generated contracts.
|
|
"packages/paperclip-runner/generated",
|
|
"packages/paperclip-runner/protocol",
|
|
"packages/paperclip-runner/runner/Cargo.lock",
|
|
"packages/paperclip-runner/runner/Cargo.toml",
|
|
"packages/paperclip-runner/runner/crates",
|
|
"packages/paperclip-runner/scripts/acpx-sidecar-contract.mjs",
|
|
"packages/paperclip-runner/scripts/build-provider-pack.mjs",
|
|
"packages/paperclip-runner/scripts/contracts/contract-fingerprints.mjs",
|
|
"packages/paperclip-runner/scripts/provider-pack-executable-shims.mjs",
|
|
"packages/paperclip-runner/scripts/build-node-startup-timeout.mjs",
|
|
"packages/paperclip-runner/scripts/candidate-provider-pack.mjs",
|
|
"packages/paperclip-runner/scripts/build-copilot-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/copilot-inner-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/copilot-profile-identity.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-copilot-binary.mjs",
|
|
"packages/paperclip-runner/scripts/install-copilot-assets.mjs",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v12-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v14-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v21-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v24-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v25-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v26-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v35-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v40-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v42-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v46-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v47-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v48-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v49-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v50-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v51-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v52-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v54-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v55-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v56-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v57-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v58-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v59-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v61-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v65-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v66-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v67-identity.json",
|
|
"packages/paperclip-runner/test/fixtures/copilot-profile-v68-identity.json",
|
|
"packages/paperclip-runner/scripts/materialize-cursor-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/cursor-runtime-patch.mjs",
|
|
"packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs",
|
|
"packages/paperclip-runner/scripts/provider-pack-runtime-tree.mjs",
|
|
"packages/paperclip-runner/scripts/generate-acpx-sidecar-contract.mjs",
|
|
"packages/paperclip-runner/scripts/generate-protocol-schema-module.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-pi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/pi-distribution",
|
|
"packages/paperclip-runner/scripts/cursor-native-usage.mjs",
|
|
"packages/paperclip-runner/scripts/cursor-native-usage-source.json",
|
|
"packages/paperclip-runner/scripts/provision-cursor.mjs",
|
|
"packages/paperclip-runner/cursor-distributions.json",
|
|
"packages/paperclip-runner/cursor-contract.json",
|
|
// Pi runtime/extension/Node/closure pins are included by the src tree below.
|
|
"packages/paperclip-runner/src",
|
|
"packages/paperclip-runner/styles.css",
|
|
"packages/paperclip-runner/tsconfig.json",
|
|
"packages/paperclip-runner/tsconfig.surfaces.json",
|
|
])] as const;
|
|
|
|
const ignoredRunnerDevelopmentDirectoryPaths = new Set([
|
|
"packages/paperclip-runner/devtools",
|
|
"packages/paperclip-runner/docs",
|
|
"packages/paperclip-runner/examples",
|
|
"packages/paperclip-runner/test",
|
|
"packages/paperclip-runner/test-fixtures",
|
|
"packages/paperclip-runner/test-support",
|
|
]);
|
|
|
|
const runnerDocumentationFilePattern = /\.md$/;
|
|
const runnerTestFilePattern = /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/;
|
|
const runnerRustIntegrationTestPathPattern =
|
|
/^packages\/paperclip-runner\/runner\/crates\/[^/]+\/tests(?:\/|$)/;
|
|
const runnerSmokeScriptPattern =
|
|
/^packages\/paperclip-runner\/scripts\/[^/]+-smoke\.mjs$/;
|
|
|
|
export interface DaytonaImageContentOptions {
|
|
repositoryRoot?: string;
|
|
inputPaths?: readonly string[];
|
|
platform?: string;
|
|
baseImages?: readonly string[];
|
|
frontendDigest?: string;
|
|
/** Must match the Docker PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS build argument. */
|
|
candidateProviders?: readonly string[];
|
|
}
|
|
|
|
export function normalizedDaytonaCandidateProviders(values: readonly string[]): string[] {
|
|
const selected = [...values].sort();
|
|
if (selected.some(value => !["cursor", "copilot", "pi"].includes(value))
|
|
|| new Set(selected).size !== selected.length) {
|
|
throw new Error("Daytona candidate providers must be distinct known ACP profiles");
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
function compareNames(left: string, right: string): number {
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
}
|
|
|
|
function normalizedRelativePath(value: string): string {
|
|
return value.split(path.sep).join("/");
|
|
}
|
|
|
|
function shouldIgnore(relativePath: string): boolean {
|
|
return relativePath.split("/").includes("node_modules");
|
|
}
|
|
|
|
function shouldIgnoreRunnerDevelopmentInput(relativePath: string): boolean {
|
|
if (!relativePath.startsWith("packages/paperclip-runner/")) return false;
|
|
if (ignoredRunnerDevelopmentDirectoryPaths.has(relativePath)) return true;
|
|
return (
|
|
runnerDocumentationFilePattern.test(relativePath) ||
|
|
runnerTestFilePattern.test(relativePath) ||
|
|
runnerRustIntegrationTestPathPattern.test(relativePath) ||
|
|
runnerSmokeScriptPattern.test(relativePath)
|
|
);
|
|
}
|
|
|
|
function updateRecord(
|
|
hash: ReturnType<typeof createHash>,
|
|
kind: string,
|
|
relativePath: string,
|
|
payload = "",
|
|
): void {
|
|
hash.update(kind);
|
|
hash.update("\0");
|
|
hash.update(relativePath);
|
|
hash.update("\0");
|
|
hash.update(payload);
|
|
hash.update("\0");
|
|
}
|
|
|
|
function assertPinnedBaseImage(reference: string): void {
|
|
if (!/@sha256:[0-9a-f]{64}$/.test(reference)) {
|
|
throw new Error(
|
|
`Daytona image base must use an immutable sha256 digest: ${reference}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertPinnedFrontendDigest(digest: string): void {
|
|
if (!/^sha256:[0-9a-f]{64}$/.test(digest)) {
|
|
throw new Error(
|
|
`Daytona Dockerfile frontend must use an immutable sha256 digest: ${digest}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function extractDaytonaDockerfileFrontendDigest(
|
|
dockerfile: string,
|
|
): string {
|
|
const firstLine = dockerfile.split(/\r?\n/, 1)[0] ?? "";
|
|
const match = /^#\s*syntax=\S+@(sha256:[0-9a-f]{64})\s*$/.exec(firstLine);
|
|
if (!match) {
|
|
throw new Error(
|
|
"Daytona Dockerfile first line must pin its syntax frontend to an immutable sha256 digest",
|
|
);
|
|
}
|
|
const digest = match[1]!;
|
|
assertPinnedFrontendDigest(digest);
|
|
return digest;
|
|
}
|
|
|
|
export function extractDaytonaBaseImages(dockerfile: string): string[] {
|
|
const baseImages: string[] = [];
|
|
const stageAliases = new Set<string>();
|
|
|
|
for (const line of dockerfile.split(/\r?\n/)) {
|
|
if (!/^\s*FROM\s/i.test(line)) continue;
|
|
const match = /^\s*FROM(?:\s+--\S+)*\s+(\S+)(?:\s+AS\s+(\S+))?\s*$/i.exec(
|
|
line,
|
|
);
|
|
if (!match) {
|
|
throw new Error(`Cannot parse Daytona Dockerfile FROM line: ${line}`);
|
|
}
|
|
|
|
const reference = match[1]!;
|
|
if (!stageAliases.has(reference)) {
|
|
assertPinnedBaseImage(reference);
|
|
baseImages.push(reference);
|
|
}
|
|
if (match[2]) stageAliases.add(match[2]);
|
|
}
|
|
|
|
if (baseImages.length === 0) {
|
|
throw new Error("Daytona Dockerfile does not declare a base image");
|
|
}
|
|
return baseImages;
|
|
}
|
|
|
|
async function resolveBaseImages(
|
|
root: string,
|
|
explicitBaseImages?: readonly string[],
|
|
): Promise<string[]> {
|
|
const baseImages = explicitBaseImages
|
|
? [...explicitBaseImages]
|
|
: extractDaytonaBaseImages(
|
|
await readFile(path.join(root, DAYTONA_IMAGE_DOCKERFILE_PATH), "utf8"),
|
|
);
|
|
if (baseImages.length === 0) {
|
|
throw new Error("Daytona image content identity requires a base image");
|
|
}
|
|
for (const reference of baseImages) assertPinnedBaseImage(reference);
|
|
return baseImages.sort(compareNames);
|
|
}
|
|
|
|
async function resolveFrontendDigest(
|
|
root: string,
|
|
explicitFrontendDigest?: string,
|
|
): Promise<string> {
|
|
const digest =
|
|
explicitFrontendDigest ??
|
|
extractDaytonaDockerfileFrontendDigest(
|
|
await readFile(path.join(root, DAYTONA_IMAGE_DOCKERFILE_PATH), "utf8"),
|
|
);
|
|
assertPinnedFrontendDigest(digest);
|
|
return digest;
|
|
}
|
|
|
|
async function hashEntry(
|
|
hash: ReturnType<typeof createHash>,
|
|
root: string,
|
|
relativePath: string,
|
|
): Promise<void> {
|
|
const normalizedPath = normalizedRelativePath(relativePath);
|
|
if (
|
|
shouldIgnore(normalizedPath) ||
|
|
shouldIgnoreRunnerDevelopmentInput(normalizedPath)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const absolutePath = path.resolve(root, relativePath);
|
|
const relativeFromRoot = path.relative(root, absolutePath);
|
|
if (
|
|
relativeFromRoot.startsWith(`..${path.sep}`) ||
|
|
relativeFromRoot === ".." ||
|
|
path.isAbsolute(relativeFromRoot)
|
|
) {
|
|
throw new Error(
|
|
`Daytona image input escapes repository root: ${relativePath}`,
|
|
);
|
|
}
|
|
|
|
const stats = await lstat(absolutePath);
|
|
if (stats.isDirectory()) {
|
|
const entries = await readdir(absolutePath, { withFileTypes: true });
|
|
entries.sort((left, right) => compareNames(left.name, right.name));
|
|
for (const entry of entries) {
|
|
await hashEntry(hash, root, path.join(relativePath, entry.name));
|
|
}
|
|
return;
|
|
}
|
|
if (stats.isSymbolicLink()) {
|
|
updateRecord(hash, "symlink", normalizedPath, await readlink(absolutePath));
|
|
return;
|
|
}
|
|
if (stats.isFile()) {
|
|
const executable =
|
|
(stats.mode & 0o111) === 0 ? "non-executable" : "executable";
|
|
updateRecord(hash, "file", normalizedPath, executable);
|
|
hash.update(await readFile(absolutePath));
|
|
hash.update("\0");
|
|
return;
|
|
}
|
|
throw new Error(`Unsupported Daytona image input type: ${relativePath}`);
|
|
}
|
|
|
|
export async function computeDaytonaImageContentId(
|
|
options: DaytonaImageContentOptions = {},
|
|
): Promise<string> {
|
|
const root = path.resolve(options.repositoryRoot ?? repositoryRoot);
|
|
const inputPaths = [
|
|
...(options.inputPaths ?? DAYTONA_IMAGE_INPUT_PATHS),
|
|
].sort(compareNames);
|
|
const hash = createHash("sha256");
|
|
updateRecord(
|
|
hash,
|
|
"contract",
|
|
DAYTONA_IMAGE_CONTENT_SCHEMA,
|
|
options.platform ?? DAYTONA_IMAGE_PLATFORM,
|
|
);
|
|
updateRecord(
|
|
hash,
|
|
"dockerfile-frontend",
|
|
await resolveFrontendDigest(root, options.frontendDigest),
|
|
);
|
|
for (const baseImage of await resolveBaseImages(root, options.baseImages)) {
|
|
updateRecord(hash, "base-image", baseImage);
|
|
}
|
|
for (const candidate of normalizedDaytonaCandidateProviders(options.candidateProviders ?? [])) {
|
|
updateRecord(hash, "candidate-provider", candidate);
|
|
}
|
|
for (const inputPath of inputPaths) {
|
|
await hashEntry(hash, root, inputPath);
|
|
}
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
|
|
const arguments_ = process.argv.slice(2);
|
|
const candidateFlag = arguments_[0];
|
|
if (arguments_.length > 1 || (candidateFlag !== undefined && !candidateFlag.startsWith("--candidate-providers="))) {
|
|
throw new Error("Expected only --candidate-providers=cursor,copilot,pi");
|
|
}
|
|
const candidateProviders = candidateFlag?.slice("--candidate-providers=".length).split(",").filter(Boolean) ?? [];
|
|
computeDaytonaImageContentId({ candidateProviders })
|
|
.then((contentId) => process.stdout.write(`${contentId}\n`))
|
|
.catch((error: unknown) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
export const DAYTONA_IMAGE_CONTENT_SCRIPT = fileURLToPath(import.meta.url);
|