Build staging migrator artifacts and align native sandbox sessions with HOME
Use object-storage artifact URLs and Actions build artifacts without creating releases. Fix both native session entry points, preserve the primary workspace environment, and declare upload headers in OpenAPI. Tests cover sandbox home confinement, session dispatch, and generated headers. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
d3e4c2e031
commit
639d8875bb
|
|
@ -13,6 +13,11 @@ on:
|
|||
# the new tag ref instead. The tag mapping below keys off github.ref either
|
||||
# way.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
staging_artifact_base_url:
|
||||
description: Optional HTTPS object-storage/CDN prefix for staging migrator artifacts (build only; no releases)
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -26,6 +31,39 @@ concurrency:
|
|||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
staging-migrator:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.staging_artifact_base_url != ''
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Require a non-default branch
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$REF" == refs/heads/* && "$REF" != "refs/heads/$DEFAULT_BRANCH" ]]
|
||||
- uses: actions/checkout@v7
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build matching migrator artifacts
|
||||
env:
|
||||
ARTIFACT_BASE_URL: ${{ inputs.staging_artifact_base_url }}
|
||||
run: node scripts/build-preview-migrator.mjs "$RUNNER_TEMP/staging-migrator" "$ARTIFACT_BASE_URL"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: staging-migrator-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/staging-migrator/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
|
|
|||
|
|
@ -137,10 +137,17 @@ interrupted saves, and recovery without the original sandbox or app volume.
|
|||
Passing acceptance does not authorize a merge or mainline release. Both require
|
||||
the user's explicit sign-off.
|
||||
|
||||
Staging migrator artifacts must use a staging-only distribution path; do not
|
||||
create GitHub releases to transport them. The initial prerelease publication
|
||||
job has been removed. Its replacement remains pending, so the existing
|
||||
release-backed preview resolver is not an approved deployment path. The
|
||||
replacement must preserve commit identity, artifact integrity, migration
|
||||
coverage and dependency-lockfile checks, and restrict deployment to explicitly
|
||||
selected pinned staging stacks without changing the fleet default.
|
||||
Staging migrator artifacts use an immutable object-storage prefix. The Docker
|
||||
workflow's optional `staging_artifact_base_url` input builds DB/shared tarballs
|
||||
and an integrity manifest as a GitHub Actions artifact; it has no release-write
|
||||
permission. Transfer those artifacts to the staging bucket using conditional
|
||||
creates, publishing the manifest last. Do not create GitHub releases or publish
|
||||
npm packages for this flow.
|
||||
|
||||
Cloud enables this lane only in staging through
|
||||
`CLOUD_HARNESS_STAGING_ARTIFACT_BASE_URL`. Resolve `preview:<full SHA>` through
|
||||
its authenticated deployment API, then target the dedicated pinned stack.
|
||||
The configured origin, commit identity, artifact integrity, migration coverage,
|
||||
dependency lockfile and tenant readiness are checked before acceptance. Preview
|
||||
artifacts cannot become the fleet default. Record both the build workflow and
|
||||
the resulting object identities with the acceptance evidence.
|
||||
|
|
|
|||
|
|
@ -161,6 +161,21 @@ describe("Codex value and workspace boundaries", () => {
|
|||
).toThrow("filesystem root");
|
||||
});
|
||||
|
||||
it("accepts only the host-bound scoped home in external sandboxes", () => {
|
||||
const home = "/home/daytona";
|
||||
const env = { HOME: home, PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "1",
|
||||
PAPERCLIP_WORKSPACE_CWD: `${home}/repos/main`, PAPERCLIP_PRIMARY_REPO: `${home}/repos/main`,
|
||||
PAPERCLIP_TASK_DIR: `${home}/task`, PAPERCLIP_AGENT_DIR: `${home}/agent`,
|
||||
PAPERCLIP_USER_DIR: `${home}/user`, PAPERCLIP_PROJECT_DIR: `${home}/project`, PAPERCLIP_REPOS_DIR: `${home}/repos` };
|
||||
expect(validateCodexWorkingDirectory(home, env, "remote_runner")).toBe(home);
|
||||
for (const cwd of ["/home", `${home}/.codex`, `${home}/repos/other`]) {
|
||||
expect(() => validateCodexWorkingDirectory(cwd, env, "remote_runner")).toThrow("does not match");
|
||||
}
|
||||
expect(() => validateCodexWorkingDirectory(home, { ...env, PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: undefined }, "remote_runner")).toThrow("does not match");
|
||||
expect(() => validateCodexWorkingDirectory(home, { ...env, PAPERCLIP_PRIMARY_REPO: `${home}/repos/other` }, "remote_runner")).toThrow("does not match");
|
||||
expect(() => validateCodexWorkingDirectory(home, { ...env, PAPERCLIP_USER_DIR: "/etc" }, "remote_runner")).toThrow("does not match its home");
|
||||
});
|
||||
|
||||
it("bounds retained values and redacts protected diagnostics", () => {
|
||||
const bounded = boundedCodexPayload({
|
||||
short: "ok",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { externalWorkFolderEnvironment } from "../../work-folder-environment.js";
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import {
|
||||
basename,
|
||||
|
|
@ -150,7 +151,10 @@ function validateRemoteRunnerWorkingDirectory(
|
|||
// The controller cannot inspect a provider-owned filesystem. Pin the facade
|
||||
// to the exact remote workspace while runnerd validates existence, type, and
|
||||
// canonical identity inside the authoritative filesystem before launch.
|
||||
if (workingDirectory !== configuredRoot) {
|
||||
const scoped = externalWorkFolderEnvironment(environment);
|
||||
const scopedHome = scoped.HOME;
|
||||
const hostBoundHome = scopedHome && scoped.PAPERCLIP_PRIMARY_REPO === configuredRoot;
|
||||
if (workingDirectory !== configuredRoot && !(hostBoundHome && workingDirectory === scopedHome)) {
|
||||
throw new Error(
|
||||
"Remote Codex working directory does not match the assigned workspace",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,21 +7,23 @@ import path from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { materializePublishManifest, prepareBundledPackage } from "./prepare-bundled-package.mjs";
|
||||
|
||||
export function previewIdentity(sha, date, repository = "paperclipai/paperclip") {
|
||||
export function previewIdentity(sha, date, artifactBaseUrl) {
|
||||
if (!/^[a-f0-9]{40}$/.test(sha) || Number.isNaN(date.getTime())) throw new Error("Invalid preview commit");
|
||||
const base = new URL(artifactBaseUrl);
|
||||
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash) throw new Error("Invalid staging artifact base URL");
|
||||
const day = `${date.getUTCMonth() + 1}${String(date.getUTCDate()).padStart(2, "0")}`;
|
||||
const second = date.getUTCHours() * 3600 + date.getUTCMinutes() * 60 + date.getUTCSeconds() + 1;
|
||||
return { tag: `preview/${sha}`, version: `${date.getUTCFullYear()}.${day}.${second}-preview.sha${sha}`,
|
||||
baseUrl: `https://github.com/${repository}/releases/download/${encodeURIComponent(`preview/${sha}`)}` };
|
||||
baseUrl: `${base.href.replace(/\/$/, "")}/${sha}` };
|
||||
}
|
||||
|
||||
export function buildPreviewMigrator(outputDirectory) {
|
||||
export function buildPreviewMigrator(outputDirectory, artifactBaseUrl) {
|
||||
const repo = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const git = (...args) => execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim();
|
||||
const sha = git("rev-parse", "HEAD");
|
||||
if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== sha) throw new Error("Preview checkout differs from the workflow commit");
|
||||
git("diff", "--quiet", "HEAD");
|
||||
const identity = previewIdentity(sha, new Date(git("show", "-s", "--format=%cI", "HEAD")));
|
||||
const identity = previewIdentity(sha, new Date(git("show", "-s", "--format=%cI", "HEAD")), artifactBaseUrl);
|
||||
execFileSync("pnpm", ["--filter", "@paperclipai/db...", "build"], { cwd: repo, stdio: "inherit" });
|
||||
const output = path.resolve(outputDirectory);
|
||||
mkdirSync(output, { recursive: true });
|
||||
|
|
@ -58,6 +60,6 @@ export function buildPreviewMigrator(outputDirectory) {
|
|||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
if (!process.argv[2]) throw new Error("Usage: build-preview-migrator.mjs <new output directory>");
|
||||
console.log(JSON.stringify(buildPreviewMigrator(process.argv[2])));
|
||||
if (!process.argv[2] || !process.argv[3]) throw new Error("Usage: build-preview-migrator.mjs <new output directory> <staging artifact base URL>");
|
||||
console.log(JSON.stringify(buildPreviewMigrator(process.argv[2], process.argv[3])));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ import { previewIdentity } from "./build-preview-migrator.mjs";
|
|||
|
||||
test("preview artifact identity is immutable, namespaced, and ordered by commit time", () => {
|
||||
const sha = "2f42a4968d5761fd62172e35ecf8188195b8d431";
|
||||
const identity = previewIdentity(sha, new Date("2026-07-19T09:30:00.000Z"));
|
||||
const identity = previewIdentity(sha, new Date("2026-07-19T09:30:00.000Z"), "https://staging.example/migrators");
|
||||
assert.equal(identity.version, `2026.719.34201-preview.sha${sha}`);
|
||||
assert.equal(identity.tag, `preview/${sha}`);
|
||||
assert.equal(identity.baseUrl, `https://github.com/paperclipai/paperclip/releases/download/preview%2F${sha}`);
|
||||
assert.equal(identity.baseUrl, `https://staging.example/migrators/${sha}`);
|
||||
assert.throws(() => previewIdentity("master", new Date()), /Invalid preview/);
|
||||
assert.throws(() => previewIdentity(sha, new Date("invalid")), /Invalid preview/);
|
||||
});
|
||||
|
||||
test("staging artifact URLs cannot carry credentials or use plaintext", () => {
|
||||
for (const url of ["http://staging.example", "https://user:secret@staging.example", "https://staging.example?token=secret"]) {
|
||||
assert.throws(() => previewIdentity("a".repeat(40), new Date(), url), /Invalid staging/);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -405,3 +405,16 @@ describe("openapi routes", () => {
|
|||
expect(codes).toEqual(["200", "401", "403", "404"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("declares work-folder retry and executable headers for generated clients", () => {
|
||||
const spec = buildOpenApiSpec();
|
||||
const root = "/api/companies/{companyId}/work-folders/{scope}/{ownerId}";
|
||||
expect(spec.paths[`${root}/content`].put.parameters).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: "Idempotency-Key", in: "header", required: false }),
|
||||
expect.objectContaining({ name: "X-File-Executable", in: "header", schema: { type: "string", enum: ["true", "false"] } }),
|
||||
expect.objectContaining({ name: "X-File-Content-Type", in: "header" }),
|
||||
]));
|
||||
expect(spec.paths[`${root}/operations`].post.parameters).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: "Idempotency-Key", in: "header" }),
|
||||
]));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ type OpenApiPathRegistration = {
|
|||
request?: {
|
||||
params?: z.ZodTypeAny;
|
||||
query?: z.ZodTypeAny;
|
||||
headers?: z.ZodTypeAny;
|
||||
body?: {
|
||||
content: Record<string, { schema: unknown }>;
|
||||
required?: boolean;
|
||||
|
|
@ -487,7 +488,7 @@ function normalizeResponses(responses: Record<string, OpenApiResponse> = {}) {
|
|||
);
|
||||
}
|
||||
|
||||
function parametersFromSchema(schema: z.ZodTypeAny, location: "path" | "query") {
|
||||
function parametersFromSchema(schema: z.ZodTypeAny, location: "path" | "query" | "header") {
|
||||
const objectSchema = unwrapSchema(schema);
|
||||
if (zodTypeName(objectSchema) !== "object") return [];
|
||||
const shape = zodDef(objectSchema).shape as Record<string, z.ZodTypeAny>;
|
||||
|
|
@ -528,6 +529,12 @@ class OpenAPIRegistry {
|
|||
...parametersFromSchema(request.query, "query"),
|
||||
];
|
||||
}
|
||||
if (request?.headers) {
|
||||
normalizedOperation.parameters = [
|
||||
...((normalizedOperation.parameters as unknown[]) ?? []),
|
||||
...parametersFromSchema(request.headers, "header"),
|
||||
];
|
||||
}
|
||||
if (request?.body) {
|
||||
normalizedOperation.requestBody = {
|
||||
...request.body,
|
||||
|
|
@ -5277,12 +5284,15 @@ registry.registerPath({ method: "get", path: `${workFolderPath}/content`, tags:
|
|||
});
|
||||
registry.registerPath({ method: "put", path: `${workFolderPath}/content`, tags: ["work-folders"], summary: "Upload or replace a scoped file",
|
||||
description: "Send raw bytes as application/octet-stream, including an empty body for an empty file. Idempotency-Key identifies a retry. X-File-Executable: true preserves executable permission. X-File-Content-Type specifies the stored media type.",
|
||||
request: { params: workFolderParams, query: z.object({ path: z.string() }), body: { required: true, content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } } } },
|
||||
request: { params: workFolderParams, query: z.object({ path: z.string() }), headers: z.object({
|
||||
"Idempotency-Key": z.string().optional(), "X-File-Executable": z.enum(["true", "false"]).optional(),
|
||||
"X-File-Content-Type": z.string().optional(),
|
||||
}), body: { required: true, content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } } } },
|
||||
responses: { ...workFolderErrors, 200: r.ok() },
|
||||
});
|
||||
registry.registerPath({ method: "post", path: `${workFolderPath}/operations`, tags: ["work-folders"], summary: "Create a directory, delete, restore, or permanently purge files",
|
||||
description: "Deletion retains a recoverable copy. Restore rejects occupied paths. Purge removes the deleted copy permanently. Idempotency-Key identifies retries.",
|
||||
request: { params: workFolderParams, body: jsonBody(z.discriminatedUnion("action", [
|
||||
request: { params: workFolderParams, headers: z.object({ "Idempotency-Key": z.string().optional() }), body: jsonBody(z.discriminatedUnion("action", [
|
||||
z.object({ action: z.literal("mkdir"), path: z.string() }), z.object({ action: z.literal("delete"), path: z.string() }),
|
||||
z.object({ action: z.literal("restore"), fileId: z.uuid() }), z.object({ action: z.literal("purge"), fileId: z.uuid() }),
|
||||
])) }, responses: { ...workFolderErrors, 200: r.ok(z.object({ applied: z.boolean() })) },
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js";
|
||||
|
||||
type BackendFactoryOptions = {
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
runnerInstanceId?: string;
|
||||
acpxRuntimeDirectory?: string;
|
||||
workingDirectoryAuthority?: "local_filesystem" | "remote_runner";
|
||||
|
|
@ -6011,6 +6012,88 @@ describe("runnerd provider runtime wiring", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("starts sandbox sessions in the scoped home while preserving the primary workspace", async () => {
|
||||
const remoteCwd = "/home/daytona/repos/main";
|
||||
const home = "/home/daytona";
|
||||
const remoteExecution = {
|
||||
...execution,
|
||||
binding: { ...execution.binding, runId: "run-scoped-home-test" },
|
||||
task: {
|
||||
identifier: "DOT-REMOTE",
|
||||
title: "Remote workspace test",
|
||||
description: null,
|
||||
prompt: "Verify the remote workspace.",
|
||||
workMode: "standard",
|
||||
},
|
||||
workspace: {
|
||||
cwd: "/host/paperclip-workspace",
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
branchName: null,
|
||||
},
|
||||
session: {
|
||||
normalizedSessionId: "scoped-home-session",
|
||||
driverKind: "codex_app_server",
|
||||
protocolVersion: 2,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
},
|
||||
provider: {
|
||||
kind: "codex",
|
||||
model: null,
|
||||
approvalPolicy: "never",
|
||||
},
|
||||
executionMode: "default",
|
||||
planningContext: null,
|
||||
interactionResponses: [],
|
||||
credentialBindings: [],
|
||||
} as unknown as NativeExecutionInputV1;
|
||||
state.createBackend.mockClear();
|
||||
state.execute.mockReset().mockResolvedValue({
|
||||
result: { summary: "completed" },
|
||||
terminal: { runTerminalState: "succeeded" },
|
||||
turnId: "turn",
|
||||
normalizedSessionId: "session",
|
||||
providerSessionId: null,
|
||||
driverKind: "test",
|
||||
driverVersion: "1",
|
||||
nativeEventCount: 1,
|
||||
highestContiguousSourceSeq: 1,
|
||||
});
|
||||
|
||||
await executePaperclipNativeSession({
|
||||
db: leaseDb(remoteExecution),
|
||||
execution: remoteExecution,
|
||||
runnerInstanceId: "runner",
|
||||
useRunnerd: true,
|
||||
runnerExecutionTarget: {
|
||||
kind: "remote", transport: "sandbox", remoteCwd, workFolderHome: home,
|
||||
environmentId: "environment", leaseId: "lease", providerKey: "daytona",
|
||||
runner: { execute: vi.fn(), syncIn: vi.fn() },
|
||||
} as never,
|
||||
runnerPublicUrl: "wss://paperclip.example.test",
|
||||
});
|
||||
|
||||
expect(state.createBackend).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspace: expect.objectContaining({ cwd: home }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
workingDirectoryAuthority: "remote_runner",
|
||||
}),
|
||||
);
|
||||
expect(state.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: expect.objectContaining({
|
||||
workspace: expect.objectContaining({ cwd: home }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(state.createBackend.mock.calls[0]![1].environment).toEqual(expect.objectContaining({
|
||||
HOME: home, CODEX_HOME: `${home}/.codex`, PAPERCLIP_WORKSPACE_CWD: remoteCwd,
|
||||
PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "1",
|
||||
}));
|
||||
});
|
||||
|
||||
it("uses the image's shared Codex without uploading or installing artifacts", async () => {
|
||||
const syncIn = vi.fn(async () => undefined);
|
||||
const remoteExecute = vi.fn(
|
||||
|
|
|
|||
|
|
@ -4344,7 +4344,9 @@ async function executePaperclipNativeSessionWithinScope(
|
|||
...input.execution,
|
||||
workspace: {
|
||||
...input.execution.workspace,
|
||||
cwd: input.runnerExecutionTarget.remoteCwd,
|
||||
cwd: input.runnerExecutionTarget.transport === "sandbox"
|
||||
? input.runnerExecutionTarget.workFolderHome ?? input.runnerExecutionTarget.remoteCwd
|
||||
: input.runnerExecutionTarget.remoteCwd,
|
||||
},
|
||||
}
|
||||
: input.execution;
|
||||
|
|
@ -7613,7 +7615,7 @@ async function createRunnerdBackendWithinSessionClaim(
|
|||
// host-home deny rules cannot shadow the assigned workspace.
|
||||
HOME: remoteTarget!.transport === "sandbox" && remoteTarget!.workFolderHome ? remoteTarget!.workFolderHome : posix.join(remoteRunnerFilesystemRoot!, "codex-home"),
|
||||
CODEX_HOME: remoteTarget!.transport === "sandbox" && remoteTarget!.workFolderHome ? posix.join(remoteTarget!.workFolderHome, ".codex") : posix.join(remoteRunnerFilesystemRoot!, "codex-home"),
|
||||
PAPERCLIP_WORKSPACE_CWD: runnerExecution.workspace.cwd,
|
||||
PAPERCLIP_WORKSPACE_CWD: remoteTarget!.remoteCwd,
|
||||
...(remoteTarget!.transport === "sandbox"
|
||||
? { PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "1" }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function isStagingOrigin(value: string) {
|
|||
export interface DeployedStack {
|
||||
baseURL: string; stackId: string; commit: string; appImage: string; migratorVersion: string; sandboxImage: string;
|
||||
companyId: string; taskId: string; agentId: string; projectId: string; userId: string;
|
||||
excludedAdapters?: Array<{ adapterType: string; reason: string }>;
|
||||
profiles: Array<{ id: string; adapterType: string; engine: string; model: string; qualification: string; agentId: string }>;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +29,12 @@ export function loadDeployedStack(): DeployedStack {
|
|||
assert(/@sha256:[a-f0-9]{64}$/.test(manifest.sandboxImage), "Expected immutable sandbox image");
|
||||
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
||||
for (const key of ["companyId", "taskId", "agentId", "projectId"] as const) assert(uuid.test(manifest[key]), `Invalid ${key}`);
|
||||
if (manifest.excludedAdapters !== undefined) {
|
||||
assert(Array.isArray(manifest.excludedAdapters), "Invalid adapter exclusions");
|
||||
for (const exclusion of manifest.excludedAdapters) {
|
||||
assert(typeof exclusion.adapterType === "string" && typeof exclusion.reason === "string" && exclusion.reason.trim().length > 0, "Exclusions require an explicit reason");
|
||||
}
|
||||
}
|
||||
assert(Array.isArray(manifest.profiles) && manifest.profiles.length >= 7, "The seven baseline profiles are required");
|
||||
for (const profile of manifest.profiles) {
|
||||
for (const key of ["id", "adapterType", "engine", "model", "qualification", "agentId"] as const) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ test("deployed candidate and complete supported adapter inventory", async ({}, i
|
|||
expect(capabilities.sandboxProviders.daytona?.supportsRunExecution).toBe(true);
|
||||
const adapters = await api.json<Array<{ type: string; disabled: boolean; capabilities: { supportsAcp: boolean } }>>("/api/adapters");
|
||||
const required: string[] = [];
|
||||
for (const adapter of adapters.filter((entry) => !entry.disabled)) {
|
||||
const excluded = new Set(stack.excludedAdapters?.map((entry) => entry.adapterType));
|
||||
for (const adapter of adapters.filter((entry) => !entry.disabled && !excluded.has(entry.type))) {
|
||||
if (capabilities.adapters.find((entry) => entry.adapterType === adapter.type)?.drivers.sandbox !== "supported") continue;
|
||||
if (adapter.type === "paperclip_runner") {
|
||||
required.push("paperclip_runner:codex", "paperclip_runner:opencode",
|
||||
|
|
|
|||
Loading…
Reference in New Issue