mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
feat: include Linux ARM64 in native Codex runner targets
Ship a source-bound ARM64 daemon alongside Linux x64 and both Mac targets. Use native hosted builds and the existing clean npm/native startup verifier. Read target requirements from the immutable source to preserve historical three-target promotions. Keep Codex payloads as official npm dependencies. Resolve ARM64 creation and login through the shared platform contract. Share the existing legacy CLI permission calculation with import previews without changing execution behavior. Record targeted checks and live Mac evidence. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
058af8f1b6
commit
dcdf09a0f3
24 files changed
+363
-98
No files matched your search
@@ -24,7 +24,7 @@ on:
|
||||
default: release-smoke
|
||||
type: string
|
||||
qualification_source_sha:
|
||||
description: Optional immutable source SHA for provider-free three-target package qualification
|
||||
description: Optional immutable source SHA for provider-free native package qualification
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
qualification_mac:
|
||||
qualification_native:
|
||||
if: ${{ !cancelled() && inputs.qualification_source_sha != '' && needs.qualification_daemons.result == 'success' }}
|
||||
needs: [qualification_daemons, qualification_pack]
|
||||
name: Clean installed package (${{ matrix.target }})
|
||||
@@ -290,12 +290,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: darwin-arm64
|
||||
host: macos-15
|
||||
- target: darwin-x64
|
||||
host: macos-15-intel
|
||||
matrix: ${{ fromJSON(needs.qualification_daemons.outputs.native_consumer_matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -321,12 +316,14 @@ jobs:
|
||||
set -euo pipefail
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
|
||||
node scripts/verify-grok-npm-install.mjs --mac-consumer \
|
||||
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/mac-public-install.json"
|
||||
mode=--mac-consumer
|
||||
if [[ "$TARGET" == linux-arm64 ]]; then mode=--linux-arm64-consumer; fi
|
||||
node scripts/verify-grok-npm-install.mjs "$mode" \
|
||||
"$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/native-public-install.json"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}-${{ matrix.target }}
|
||||
path: ${{ runner.temp }}/mac-public-install.json
|
||||
path: ${{ runner.temp }}/native-public-install.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
outputs:
|
||||
runner_artifact_prefix:
|
||||
value: ${{ jobs.runner_binaries.outputs.artifact_prefix }}
|
||||
native_consumer_matrix:
|
||||
value: ${{ jobs.runner_binary_targets.outputs.native_consumers }}
|
||||
|
||||
# Caller-provided refs may name unmerged PR code. AWS is eligible only when
|
||||
# the caller runs on canonical master and verifies that event's exact SHA.
|
||||
@@ -459,8 +461,48 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
runner_binary_targets:
|
||||
if: inputs.runner_binaries_only
|
||||
name: Read exact-source daemon targets
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.targets.outputs.matrix }}
|
||||
native_consumers: ${{ steps.targets.outputs.native_consumers }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Preserve the immutable source's platform contract
|
||||
id: targets
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1
|
||||
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
|
||||
node --input-type=module <<'NODE'
|
||||
import assert from 'node:assert/strict';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
import { RELEASE_RUNNER_TARGETS } from './packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs';
|
||||
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
||||
const current = [...historical, 'linux-arm64'];
|
||||
const targets = [...RELEASE_RUNNER_TARGETS].sort();
|
||||
assert.ok(JSON.stringify(targets) === JSON.stringify(historical.sort()) || JSON.stringify(targets) === JSON.stringify(current.sort()), 'Unknown exact-source daemon platform contract');
|
||||
const hosts = { 'linux-x64': 'ubuntu-latest', 'linux-arm64': 'ubuntu-24.04-arm', 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' };
|
||||
const include = RELEASE_RUNNER_TARGETS.map(target => ({ target, host: hosts[target] }));
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\nnative_consumers=${JSON.stringify({ include: include.filter(row => row.target !== 'linux-x64') })}\n`);
|
||||
NODE
|
||||
|
||||
runner_binaries:
|
||||
if: inputs.runner_binaries_only
|
||||
needs: runner_binary_targets
|
||||
name: Release daemon (${{ matrix.target }})
|
||||
runs-on: ${{ matrix.host }}
|
||||
timeout-minutes: 30
|
||||
@@ -468,14 +510,7 @@ jobs:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: linux-x64
|
||||
host: ubuntu-latest
|
||||
- target: darwin-arm64
|
||||
host: macos-15
|
||||
- target: darwin-x64
|
||||
host: macos-15-intel
|
||||
matrix: ${{ fromJSON(needs.runner_binary_targets.outputs.matrix) }}
|
||||
outputs:
|
||||
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
|
||||
steps:
|
||||
|
||||
@@ -342,7 +342,7 @@ jobs:
|
||||
|
||||
release_runner_source:
|
||||
# Old promotions retain their already-shipped packaging contract. New
|
||||
# sources opt into mandatory three-target assembly in their release.sh.
|
||||
# sources opt into source-declared native target assembly in their release.sh.
|
||||
needs: [verify_canary, select_nightly, select_beta, preflight_stable]
|
||||
if: >-
|
||||
!cancelled() && (
|
||||
|
||||
@@ -2,6 +2,65 @@
|
||||
|
||||
Updated 2026-10-12.
|
||||
|
||||
## Current addition: Linux ARM64
|
||||
|
||||
The user now requires ARM Linux support as well. Treat ARM Linux as ARM64
|
||||
(`aarch64`), alongside Linux x64 and both macOS architectures. The earlier
|
||||
Linux ARM deferral below is superseded for ARM64; 32-bit ARM and Windows retain
|
||||
legacy defaults. Existing agents keep their saved execution.
|
||||
|
||||
Extend the same native Codex creation/discovery contract and existing selection,
|
||||
login, built-in and plugin fixtures. Extend the current release assembler and
|
||||
remote workflow matrix to the fourth target, with an actual ARM64 hosted build
|
||||
and clean installation. Reuse the installed no-turn Codex startup check and
|
||||
the same public npm tarballs. Read required targets from the selected source
|
||||
so historical three-target promotions still work. Keep the Linux x64 managed
|
||||
provider pack distinct from the local ARM64 daemon.
|
||||
|
||||
No local Rust/Docker builds or new paid Linux campaign. Apple Silicon task and
|
||||
follow-up acceptance remains valid at the explicitly recorded source; Linux
|
||||
ARM64 build/install/startup must acquire its own evidence. The existing
|
||||
[three-target qualifier](https://github.com/paperclipai/paperclip/actions/runs/38259364638)
|
||||
completed successfully at `d522822`, including both clean Mac consumers. It does
|
||||
not qualify ARM64. Candidate `058af8f1b69c5fa3d9bc974b9eab73c141b52823` passed
|
||||
ordinary CI and fresh 5/5 review. Its remaining import display finding is fixed
|
||||
by sharing the existing legacy argument-policy calculation with the UI without
|
||||
changing execution permissions. Ninety-five focused controls pass; all 45 exact
|
||||
argument cases also pass against the original builder. Four component cases fail
|
||||
on the original page. A no-provider protected preview shows Legacy with bypass
|
||||
off, matching `--sandbox read-only`; it does not apply another import.
|
||||
Receipt: `/private/tmp/codex-default-dc-import-live/legacy-policy-p2-receipt.json`.
|
||||
|
||||
ARM64 selection/discovery controls pass 46 and 29 cases. Fourteen isolated
|
||||
browser-login tests pass after replacing the hardcoded ARM64 legacy login branch
|
||||
with the shared platform contract. This uses native execution's owned Codex
|
||||
resolver on ARM64 and retains PATH lookup for unqualified platforms. Shared
|
||||
typecheck passes. Built-in and plugin fixtures now include ARM64; their database
|
||||
checks remain in normal Linux CI. Receipt:
|
||||
`/private/tmp/pr2-linux-arm64-selection-checkpoint.json`.
|
||||
|
||||
The packaging patch passes 97 existing controls: 15 assembler/provider-pack,
|
||||
20 consumer-boundary and 62 workflow tests. The first full workflow attempt was
|
||||
blocked by sandbox localhost permissions; the same full suite passes with its
|
||||
temporary listener access. Three workflow YAML files and MJS/shell syntax pass.
|
||||
Independent review confirms historical source compatibility and the unprivileged
|
||||
offline consumer boundaries. Official registry metadata confirms the pinned
|
||||
Node image has a native ARM64 manifest and the installed Codex dependency has
|
||||
an ARM64 package. No image or binary was pulled to this Mac. Receipt:
|
||||
`/private/tmp/pr2-linux-arm-packaging-owned-receipt.json`.
|
||||
|
||||
Actual ARM64 daemon build and clean npm/native startup remain unexecuted at this
|
||||
source freeze. The test-drive stays on its existing verified Mac daemon; its
|
||||
last restart at `058af8f` preserved both full execution digests, both recorded
|
||||
native runs and both paused imports, with the deprecated flag false. Receipt:
|
||||
`/private/tmp/codex-default-dc-import-live/final-restart-receipt.json`.
|
||||
|
||||
Next action: integrate the narrow policy display and ARM64 source/package fixes,
|
||||
run targeted controls, freeze/push one candidate, then run the existing hosted
|
||||
qualification once with all source-declared targets and require fresh CI/review.
|
||||
Keep the same branch, PR, preview and evidence index below. No merge or
|
||||
production deployment is authorized.
|
||||
|
||||
## Current amendment: include macOS in this PR
|
||||
|
||||
The user requires macOS support in the same Codex-default PR. The earlier
|
||||
|
||||
@@ -12,6 +12,30 @@ export const SANDBOX_INSTALL_COMMAND = "npm install -g @openai/codex";
|
||||
// Codex CLI warn ("Model metadata for `gpt-5.6` not found") and fall back to generic context limits.
|
||||
export const DEFAULT_CODEX_LOCAL_MODEL = PAPERCLIP_RUNNER_DEFAULT_MODELS.codex;
|
||||
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
|
||||
|
||||
/** Shared by CLI execution and configuration previews; explicit arguments keep their policy. */
|
||||
export function resolveCodexLocalCliPolicy(config: unknown, options: { networkAccess?: boolean } = {}) {
|
||||
const record = typeof config === "object" && config !== null && !Array.isArray(config)
|
||||
? config as Record<string, unknown> : {};
|
||||
const stringArgs = (value: unknown): string[] => Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string") : [];
|
||||
const configuredExtraArgs = stringArgs(record.extraArgs);
|
||||
const extraArgs = configuredExtraArgs.length > 0 ? configuredExtraArgs : stringArgs(record.args);
|
||||
const explicitSandbox = extraArgs.some((arg) =>
|
||||
/^(--sandbox(?:=|$)|-s|--profile(?:=|$)|-p|--full-auto$|--yolo$|--dangerously-bypass-approvals-and-sandbox$)/.test(arg)
|
||||
|| /^(?:(?:--config=|-c=?)\s*)?(?:sandbox_mode|profile)\s*=/.test(arg),
|
||||
);
|
||||
const explicitPermissionRestriction = extraArgs.some((arg) =>
|
||||
/^(?:(?:--config=|-c=?)\s*)?(?:approval_policy\s*=|sandbox_workspace_write\.network_access\s*=\s*false)/.test(arg),
|
||||
);
|
||||
const defaultBypass = !explicitSandbox && !explicitPermissionRestriction
|
||||
&& options.networkAccess !== false && DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
|
||||
const bypass = typeof record.dangerouslyBypassApprovalsAndSandbox === "boolean"
|
||||
? record.dangerouslyBypassApprovalsAndSandbox
|
||||
: typeof record.dangerouslyBypassSandbox === "boolean" ? record.dangerouslyBypassSandbox : defaultBypass;
|
||||
return { extraArgs, explicitSandbox, bypass };
|
||||
}
|
||||
|
||||
export const CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS = [
|
||||
"gpt-6-astra",
|
||||
"gpt-6.1-sol",
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildCodexExecArgs } from "./codex-args.js";
|
||||
import { resolveCodexLocalCliPolicy } from "../index.js";
|
||||
|
||||
describe("buildCodexExecArgs", () => {
|
||||
it.each([
|
||||
{ config: { extraArgs: ["--sandbox", "read-only"] }, bypass: false, args: ["exec", "--json", "--sandbox", "read-only", "-"] },
|
||||
{ config: { args: ["-c", "approval_policy=on-request"] }, bypass: false, args: ["exec", "--json", "-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", "-c", "approval_policy=on-request", "-"] },
|
||||
{ config: { extraArgs: [], args: ["--profile", "restricted"] }, bypass: false, args: ["exec", "--json", "--profile", "restricted", "-"] },
|
||||
{ config: { extraArgs: [0, null], args: ["--sandbox", "read-only"] }, bypass: false, args: ["exec", "--json", "--sandbox", "read-only", "-"] },
|
||||
{ config: { extraArgs: ["--color", "never"], args: ["--sandbox", "read-only"] }, bypass: true, args: ["exec", "--json", "--dangerously-bypass-approvals-and-sandbox", "--color", "never", "-"] },
|
||||
{ config: { dangerouslyBypassApprovalsAndSandbox: true, extraArgs: ["--sandbox", "read-only"] }, bypass: true, args: ["exec", "--json", "--dangerously-bypass-approvals-and-sandbox", "--sandbox", "read-only", "-"] },
|
||||
{ config: { dangerouslyBypassSandbox: true, args: ["--sandbox", "read-only"] }, bypass: true, args: ["exec", "--json", "--dangerously-bypass-approvals-and-sandbox", "--sandbox", "read-only", "-"] },
|
||||
{ config: { dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true, extraArgs: ["--color", "never"] }, bypass: false, args: ["exec", "--json", "-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", "--color", "never", "-"] },
|
||||
])("keeps the browser policy and exact execution arguments consistent for $config", ({ config, bypass, args }) => {
|
||||
expect(resolveCodexLocalCliPolicy(config).bypass).toBe(bypass);
|
||||
expect(buildCodexExecArgs(config).args).toEqual(args);
|
||||
});
|
||||
|
||||
it.each([null, "existing-session"])("defaults direct and resumed launches to full bypass (%s)", (resumeSessionId) => {
|
||||
const { args } = buildCodexExecArgs({}, { resumeSessionId });
|
||||
expect(args).toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { asBoolean, asString, asStringArray } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { asBoolean, asString } from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS,
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
resolveCodexLocalCliPolicy,
|
||||
isCodexLocalFastModeSupported,
|
||||
normalizeCodexModel,
|
||||
} from "../index.js";
|
||||
@@ -16,12 +16,6 @@ export type BuildCodexExecArgsResult = {
|
||||
fastModeIgnoredReason: string | null;
|
||||
};
|
||||
|
||||
function readExtraArgs(config: unknown): string[] {
|
||||
const fromExtraArgs = asStringArray(asRecord(config).extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(asRecord(config).args);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
@@ -49,20 +43,7 @@ export function buildCodexExecArgs(
|
||||
const search = asBoolean(record.search, false);
|
||||
const fastModeRequested = asBoolean(record.fastMode, false);
|
||||
const fastModeApplied = fastModeRequested && isCodexLocalFastModeSupported(model);
|
||||
const extraArgs = readExtraArgs(record);
|
||||
// Explicit CLI modes/profiles remain deliberate overrides. An omitted
|
||||
// setting uses the same full-auto default as agent creation and onboarding.
|
||||
const explicitSandbox = extraArgs.some((arg) =>
|
||||
/^(--sandbox(?:=|$)|-s|--profile(?:=|$)|-p|--full-auto$|--yolo$|--dangerously-bypass-approvals-and-sandbox$)/.test(arg)
|
||||
|| /^(?:(?:--config=|-c=?)\s*)?(?:sandbox_mode|profile)\s*=/.test(arg),
|
||||
);
|
||||
const explicitPermissionRestriction = extraArgs.some((arg) =>
|
||||
/^(?:(?:--config=|-c=?)\s*)?(?:approval_policy\s*=|sandbox_workspace_write\.network_access\s*=\s*false)/.test(arg),
|
||||
);
|
||||
const bypass = asBoolean(
|
||||
record.dangerouslyBypassApprovalsAndSandbox,
|
||||
asBoolean(record.dangerouslyBypassSandbox, !explicitSandbox && !explicitPermissionRestriction && options.networkAccess !== false && DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX),
|
||||
);
|
||||
const { extraArgs, explicitSandbox, bypass } = resolveCodexLocalCliPolicy(record, options);
|
||||
const args = ["exec", "--json"];
|
||||
if (!bypass && !explicitSandbox) {
|
||||
args.push("-c", 'sandbox_mode="workspace-write"');
|
||||
|
||||
@@ -56,10 +56,10 @@ test("release assembly rejects rehashed stale or missing packs before staging an
|
||||
mkdirSync(dirname(join(root, file)), { recursive: true }); cpSync(join(source, file), join(root, file));
|
||||
}
|
||||
const platforms = {};
|
||||
for (const target of ["darwin-arm64", "darwin-x64", "linux-x64"]) {
|
||||
for (const target of ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]) {
|
||||
const bytes = Buffer.alloc(64);
|
||||
if (target === "linux-x64") {
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(62, 18);
|
||||
if (target.startsWith("linux-")) {
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(target === "linux-x64" ? 62 : 183, 18);
|
||||
} else {
|
||||
bytes.writeUInt32LE(0xfeedfacf, 0); bytes.writeUInt32LE(target === "darwin-arm64" ? 0x0100000c : 0x01000007, 4);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Assemble the three independently built release daemons before npm packing.
|
||||
// Assemble the four independently built release daemons before npm packing.
|
||||
// Usage: node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs MANIFEST.json
|
||||
// Manifest: {sourceRevision, platforms: {"darwin-arm64": {path, sha256}, ...}, remoteProviderPack: {path, sha256}}.
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { runnerBinaryTarget } from "../src/live/runner-binary.ts";
|
||||
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
export const RELEASE_RUNNER_TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-x64"]);
|
||||
export const RELEASE_RUNNER_TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]);
|
||||
const required = RELEASE_RUNNER_TARGETS;
|
||||
const digest = bytes => "sha256:" + createHash("sha256").update(bytes).digest("hex");
|
||||
function regularFile(path, maximumSize) {
|
||||
@@ -38,7 +38,7 @@ export function verifyReleaseRunnerMetadata(metadata, expected) {
|
||||
|
||||
export function releaseDaemonManifest(artifactDirectory, sourceRevision, verifyMetadata) {
|
||||
if (!/^[a-f0-9]{40}$/.test(sourceRevision)) throw Error("Release daemons require an immutable source revision");
|
||||
if (readdirSync(artifactDirectory).sort().join() !== [...required].sort().join()) throw Error("Release daemons require exactly the three supported target directories");
|
||||
if (readdirSync(artifactDirectory).sort().join() !== [...required].sort().join()) throw Error("Release daemons require exactly the four supported target directories");
|
||||
const platforms = {};
|
||||
for (const target of required) {
|
||||
const directory = join(resolve(artifactDirectory), target);
|
||||
@@ -83,7 +83,7 @@ async function main() {
|
||||
const manifest = daemonOnly
|
||||
? releaseDaemonManifest(process.argv[3], process.argv[4], await metadataContract())
|
||||
: JSON.parse(regularFile(resolve(process.argv[2]), 512 * 1024));
|
||||
if (!/^[a-f0-9]{40}$/.test(manifest.sourceRevision) || !manifest.platforms || Object.keys(manifest.platforms).sort().join() !== [...required].sort().join()) throw Error("Release daemon manifest must bind one source and all three supported targets");
|
||||
if (!/^[a-f0-9]{40}$/.test(manifest.sourceRevision) || !manifest.platforms || Object.keys(manifest.platforms).sort().join() !== [...required].sort().join()) throw Error("Release daemon manifest must bind one source and all four supported targets");
|
||||
const verified = required.map(target => {
|
||||
const artifact = manifest.platforms[target];
|
||||
if (!artifact || !isAbsolute(artifact.path) || !/^sha256:[a-f0-9]{64}$/.test(artifact.sha256)) throw Error(`Invalid ${target} release artifact`);
|
||||
|
||||
@@ -16,7 +16,7 @@ const contract = { package: { name: "@paperclipai/paperclip-runner", version: "0
|
||||
const metadata = { binaryContractVersion: 2, nativeExecutionVersion: 1, harnessDriverVersion: 1,
|
||||
packageName: "@paperclipai/paperclip-runner", packageVersion: "0.0.0", prp: { name: "paperclip.runner", minimumVersion: 1, maximumVersion: 2 } };
|
||||
|
||||
test("release assembly binds all three actual targets, source, hashes and compatible metadata", async () => {
|
||||
test("release assembly binds all four actual targets, source, hashes and compatible metadata", async () => {
|
||||
const root = await realpath(await mkdtemp(path.join(tmpdir(), "runner-release-inputs-"))), source = "a".repeat(40);
|
||||
const verify = value => verifyReleaseRunnerMetadata(value, contract);
|
||||
const receiptPath = target => path.join(root, target, "receipt.json");
|
||||
@@ -24,8 +24,8 @@ test("release assembly binds all three actual targets, source, hashes and compat
|
||||
for (const target of RELEASE_RUNNER_TARGETS) {
|
||||
await mkdir(path.join(root, target));
|
||||
const bytes = Buffer.alloc(64);
|
||||
if (target === "linux-x64") {
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(62, 18);
|
||||
if (target.startsWith("linux-")) {
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(target === "linux-x64" ? 62 : 183, 18);
|
||||
} else { bytes.writeUInt32LE(0xfeedfacf); bytes.writeUInt32LE(target === "darwin-x64" ? 0x01000007 : 0x0100000c, 4); }
|
||||
await writeFile(path.join(root, target, "paperclip-runnerd"), bytes);
|
||||
await writeFile(receiptPath(target), JSON.stringify({ schema: "paperclip.runner.release-artifact.v1", sourceRevision: source,
|
||||
@@ -48,10 +48,17 @@ test("release assembly binds all three actual targets, source, hashes and compat
|
||||
await rm(binary); await symlink(path.join(root, "darwin-x64/paperclip-runnerd"), binary);
|
||||
assert.throws(() => releaseDaemonManifest(root, source, verify), /regular file/);
|
||||
await rm(binary); await writeFile(binary, bytes);
|
||||
const armBinary = path.join(root, "linux-arm64/paperclip-runnerd"), armBytes = await readFile(armBinary);
|
||||
const armReceipt = JSON.parse(await readFile(receiptPath("linux-arm64"), "utf8"));
|
||||
const wrongArm = await readFile(path.join(root, "linux-x64/paperclip-runnerd"));
|
||||
await writeFile(armBinary, wrongArm);
|
||||
await writeFile(receiptPath("linux-arm64"), JSON.stringify({ ...armReceipt, sha256: "sha256:" + createHash("sha256").update(wrongArm).digest("hex") }));
|
||||
assert.throws(() => releaseDaemonManifest(root, source, verify), /architecture mismatch/);
|
||||
await writeFile(armBinary, armBytes); await writeFile(receiptPath("linux-arm64"), JSON.stringify(armReceipt));
|
||||
await mkdir(path.join(root, "win32-x64"));
|
||||
assert.throws(() => releaseDaemonManifest(root, source, verify), /exactly the three/);
|
||||
assert.throws(() => releaseDaemonManifest(root, source, verify), /exactly the four/);
|
||||
await rm(path.join(root, "win32-x64"), { recursive: true });
|
||||
await rm(receiptPath("linux-x64"));
|
||||
await rm(receiptPath("linux-arm64"));
|
||||
assert.throws(() => releaseDaemonManifest(root, source, verify), /only its daemon and receipt/);
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -142,8 +149,8 @@ test("a source rebuild selects fresh dev bytes while preserving release binaries
|
||||
const artifact = path.join(binaryRoot, target, "paperclip-runnerd");
|
||||
if (artifact !== hostTarget) {
|
||||
const header = Buffer.alloc(64);
|
||||
if (target === "linux-x64") {
|
||||
header.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); header.writeUInt16LE(62, 18);
|
||||
if (target.startsWith("linux-")) {
|
||||
header.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); header.writeUInt16LE(target === "linux-x64" ? 62 : 183, 18);
|
||||
} else { header.writeUInt32LE(0xfeedfacf); header.writeUInt32LE(target === "darwin-x64" ? 0x01000007 : 0x0100000c, 4); }
|
||||
await mkdir(path.dirname(artifact), { recursive: true });
|
||||
await writeFile(artifact, header);
|
||||
|
||||
@@ -12,10 +12,10 @@ export function paperclipRunnerProfileForHarness(harness: string): { provider: "
|
||||
return harness === "codex_local" ? { provider: "codex" } : undefined;
|
||||
}
|
||||
|
||||
/** Creation defaults follow the shipped Linux x64 and macOS release artifacts. */
|
||||
/** Creation defaults follow the shipped 64-bit Linux and macOS release artifacts. */
|
||||
export function paperclipRunnerSupportsPlatform(harness: string, platform: string, architecture: string): boolean {
|
||||
return !!paperclipRunnerProfileForHarness(harness)
|
||||
&& ((platform === "linux" && architecture === "x64")
|
||||
&& ((platform === "linux" && (architecture === "x64" || architecture === "arm64"))
|
||||
|| (platform === "darwin" && (architecture === "arm64" || architecture === "x64")));
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ test('the bundled-dependency installs run together while the other packages are
|
||||
const checks = position("for (const tarball of tarballs) assertNoBundledCodexPayloads(run('tar', ['-tzf', tarball])");
|
||||
const copied = position('for (const tarball of tarballs) {');
|
||||
assert.ok(bundledSources < start && start < registered && registered < otherSources && otherSources < join && join < packs && packs < reordered && reordered < checks && checks < copied);
|
||||
assert.equal(source.match(/run\('npm', \['pack'/g).length, 1, 'only the lifecycle sentinel is packed in-process');
|
||||
const packagingLane = source.slice(position("} else {\nassert.equal(process.platform, 'linux'"));
|
||||
assert.equal(packagingLane.match(/run\('npm', \['pack'/g).length, 1, 'only the lifecycle sentinel is packed in-process in the packaging lane');
|
||||
assert.ok(source.includes("destination: join(root, `pack-${index}`)"), 'each pack writes into its own directory');
|
||||
// The timing line reports the split staging phase, each install, and the
|
||||
// pack step with what the packs would have cost one after another.
|
||||
@@ -352,6 +353,7 @@ test('actual tar member checks allow Codex JavaScript but reject nested platform
|
||||
assert.doesNotThrow(() => assertNoBundledCodexPayloads(javascript));
|
||||
for (const payload of [
|
||||
'package/node_modules/@openai/codex-linux-x64/package.json',
|
||||
'package/node_modules/@openai/codex-linux-arm64/package.json',
|
||||
'package/node_modules/bridge/node_modules/@openai/codex-darwin-arm64/package.json',
|
||||
'package/node_modules/@openai/codex-win32-x64/package.json',
|
||||
'package/node_modules/@openai/codex/vendor/x86_64-unknown-linux-musl/codex/codex',
|
||||
@@ -364,26 +366,28 @@ test('actual tar member checks allow Codex JavaScript but reject nested platform
|
||||
});
|
||||
|
||||
test('consumer provenance rejects bundled, foreign, inconsistent and non-official Codex payloads', () => {
|
||||
const path = 'node_modules/@openai/codex-linux-x64';
|
||||
const manifest = { name: '@openai/codex', version: '0.160.0-linux-x64', os: ['linux'], cpu: ['x64'] };
|
||||
for (const target of ['linux-x64', 'linux-arm64']) {
|
||||
const path = `node_modules/@openai/codex-${target}`;
|
||||
const manifest = { name: '@openai/codex', version: `0.160.0-${target}`, os: ['linux'], cpu: [target.slice('linux-'.length)] };
|
||||
const entry = { version: manifest.version, resolved: `https://registry.npmjs.org/@openai/codex/-/codex-${manifest.version}.tgz`,
|
||||
integrity: `sha512-${Buffer.alloc(64, 1).toString('base64')}` };
|
||||
const check = (value = entry, installed = [{ path, manifest }]) => assertInstalledCodexPlatformProvenance({ packages: { [path]: value } }, installed, 'linux-x64');
|
||||
const check = (value = entry, installed = [{ path, manifest }]) => assertInstalledCodexPlatformProvenance({ packages: { [path]: value } }, installed, target);
|
||||
assert.equal(check().codexConsumerHostOnly, true);
|
||||
const legacyManifest = { ...manifest, version: '0.156.1-linux-x64' };
|
||||
const legacyManifest = { ...manifest, version: `0.156.1-${target}` };
|
||||
assert.equal(check({ ...entry, version: legacyManifest.version,
|
||||
resolved: 'https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-linux-x64.tgz' }, [{ path, manifest: legacyManifest }]).codexConsumerHostOnly, true);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, version: '^0.160.0-linux-x64' } }]), /exact host version/);
|
||||
resolved: `https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-${target}.tgz` }, [{ path, manifest: legacyManifest }]).codexConsumerHostOnly, true);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, version: `^0.160.0-${target}` } }]), /exact host version/);
|
||||
for (const change of [
|
||||
{ inBundle: true }, { link: true }, { version: '0.156.1-linux-x64' },
|
||||
{ resolved: 'https://example.com/@openai/codex/-/codex-0.160.0-linux-x64.tgz' },
|
||||
{ resolved: 'https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-linux-x64.tgz' },
|
||||
{ resolved: 'https://user@registry.npmjs.org/@openai/codex/-/codex-0.160.0-linux-x64.tgz' },
|
||||
{ inBundle: true }, { link: true }, { version: `0.156.1-${target}` },
|
||||
{ resolved: `https://example.com/@openai/codex/-/codex-0.160.0-${target}.tgz` },
|
||||
{ resolved: `https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-${target}.tgz` },
|
||||
{ resolved: `https://user@registry.npmjs.org/@openai/codex/-/codex-0.160.0-${target}.tgz` },
|
||||
{ integrity: undefined }, { integrity: 'sha512-AA==' },
|
||||
]) assert.throws(() => check({ ...entry, ...change }));
|
||||
assert.throws(() => check(entry, []), /must install its host/);
|
||||
assert.throws(() => check(entry, [{ path: 'node_modules/@openai/codex-darwin-arm64', manifest }]), /only the host/);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, name: 'untrusted' } }]), /identity/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -413,9 +417,31 @@ test('installed daemon proof uses the production host resolver and complete sour
|
||||
assert.match(source, /runnerBinaryTarget\(bytes\), target/);
|
||||
assert.match(source, /createHash\('sha256'\)/);
|
||||
assert.match(source, /metadata.binaryContractVersion, expected.contracts.runnerdArtifact/);
|
||||
assert.match(source, /'linux-arm64'/);
|
||||
assert.doesNotMatch(source, /PAPERCLIP_RUNNERD_BINARY|runner\/target\/debug|execFileSync\('paperclip-runnerd'/);
|
||||
});
|
||||
|
||||
test('native ARM clean consumers keep the existing container and installed-runtime boundaries', () => {
|
||||
const args = grokConsumerDockerArgs({ ...paths, platform: 'linux/arm64', command: GROK_PUBLIC_INSTALL_LIFECYCLE });
|
||||
assert.deepEqual(values(args, '--platform'), ['linux/arm64']);
|
||||
assert.deepEqual(values(args, '--network'), ['none']);
|
||||
assert.deepEqual(values(args, '--user'), ['1001:1001']);
|
||||
assert.ok(args.includes('--read-only'));
|
||||
assert.deepEqual(values(args, '--cap-drop'), ['ALL']);
|
||||
assert.deepEqual(values(args, '--security-opt'), ['no-new-privileges']);
|
||||
assert.ok(values(args, '--mount').some(value => value.endsWith('dst=/packages,readonly')));
|
||||
assert.throws(() => grokConsumerDockerArgs({ ...paths, platform: 'linux/arm/v7', command: [] }), /supported native Linux platform/);
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
const arm = source.split('async function verifyLinuxArmConsumer(')[1].split("if (process.argv[2] === '--linux-arm64-consumer')")[0];
|
||||
assert.match(arm, /process.platform \+ '-' \+ process.arch, 'linux-arm64'/);
|
||||
assert.match(arm, /const platform = 'linux\/arm64'/);
|
||||
assert.match(arm, /isolated\(GROK_PUBLIC_INSTALL_LIFECYCLE\)/);
|
||||
assert.match(arm, /installedCodexProbeSource\(installed \+ '\/index.js', '\/consumer', undefined, \{ nativeCodex: true \}\)/);
|
||||
assert.match(arm, /tests\/runner-e2e\/installed-cli.ts/);
|
||||
assert.match(arm, /temporarySizeMiB: 2048, temporaryExecutable: true/);
|
||||
assert.doesNotMatch(arm, /stage-runner|runner\/target|PAPERCLIP_RUNNER_BINARY|prerequisite:/);
|
||||
});
|
||||
|
||||
|
||||
test('Mac startup wiring normalizes only validated manifest digests and uses server-scoped tooling', () => {
|
||||
const source = macInstalledStartupProbeSource('/owned/consumer', '/owned/consumer/server', 'sha256:' + 'a'.repeat(64), '/source/tests/runner-e2e/installed-cli.ts');
|
||||
|
||||
@@ -746,9 +746,14 @@ test("direct protocol concurrency override only lowers the configured ceiling",
|
||||
test("release daemon production uses exact-source native hosts and no publisher or provider permissions", () => {
|
||||
const workflow = readWorkflow("release-verify.yml");
|
||||
const producer = workflow.split(" runner_binaries:\n")[1];
|
||||
for (const [target, host] of [["linux-x64", "ubuntu-latest"], ["darwin-arm64", "macos-15"], ["darwin-x64", "macos-15-intel"]]) {
|
||||
assert.ok(producer.includes(`- target: ${target}\n host: ${host}`));
|
||||
const targets = workflow.split(" runner_binary_targets:\n")[1].split(" runner_binaries:\n")[0];
|
||||
for (const [target, host] of [["linux-x64", "ubuntu-latest"], ["linux-arm64", "ubuntu-24.04-arm"], ["darwin-arm64", "macos-15"], ["darwin-x64", "macos-15-intel"]]) {
|
||||
assert.ok(targets.includes(`'${target}': '${host}'`));
|
||||
}
|
||||
assert.match(producer, /matrix: \$\{\{ fromJSON\(needs.runner_binary_targets.outputs.matrix\) \}\}/);
|
||||
assert.match(targets, /Unknown exact-source daemon platform contract/);
|
||||
assert.match(targets, /RELEASE_RUNNER_TARGETS/);
|
||||
assert.doesNotMatch(targets, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
|
||||
assert.match(producer, /persist-credentials: false/);
|
||||
assert.match(producer, /contents: read/);
|
||||
assert.doesNotMatch(producer, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
|
||||
@@ -773,7 +778,7 @@ test("release daemon production uses exact-source native hosts and no publisher
|
||||
assert.match(script, /elif \[ "\$dry_run" = false \]; then[\s\S]*?release_fail "Public releases require/);
|
||||
});
|
||||
|
||||
test("premerge Mac qualification consumes the same Linux-packed npm bytes without publishing", () => {
|
||||
test("premerge native qualification consumes the same Linux-packed npm bytes without publishing", () => {
|
||||
const smoke = readWorkflow("release-smoke.yml");
|
||||
assert.match(smoke, /smoke_service:\n\s+if: inputs.qualification_source_sha == ''/);
|
||||
assert.match(smoke, /smoke:\n\s+if: inputs.qualification_source_sha == ''/);
|
||||
@@ -784,11 +789,46 @@ test("premerge Mac qualification consumes the same Linux-packed npm bytes withou
|
||||
assert.match(qualification, /if: \$\{\{ !cancelled\(\) && steps.pack_identity.outputs.available == 'true' \}\}/);
|
||||
assert.match(qualification, /name: \$\{\{ inputs.artifact_name \}\}-public-packages/);
|
||||
assert.match(qualification, /--mac-consumer/);
|
||||
assert.match(qualification, /host: macos-15\n/);
|
||||
assert.match(qualification, /host: macos-15-intel\n/);
|
||||
assert.match(qualification, /--linux-arm64-consumer/);
|
||||
assert.match(qualification, /fromJSON\(needs.qualification_daemons.outputs.native_consumer_matrix\)/);
|
||||
assert.match(qualification, /pnpm install --ignore-scripts --no-frozen-lockfile/);
|
||||
});
|
||||
|
||||
test("exact-source daemon matrices preserve historical three-target sources and add native ARM only for new sources", () => {
|
||||
const workflow = readWorkflow("release-verify.yml");
|
||||
const step = workflow.split(' - name: Preserve the immutable source\'s platform contract')[1].split('\n runner_binaries:')[0];
|
||||
const script = step.split(' run: |\n')[1].split('\n').map(line => line.startsWith(' ') ? line.slice(10) : line).join('\n');
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'release-target-contract-'));
|
||||
try {
|
||||
const tools = path.join(root, 'tools'); mkdirSync(tools);
|
||||
writeFileSync(path.join(tools, 'git'), '#!/bin/sh\nprintf "%s\\n" "$FIXTURE_HEAD"\n', { mode: 0o755 });
|
||||
const source = path.join(root, 'packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
|
||||
mkdirSync(path.dirname(source), { recursive: true });
|
||||
const output = path.join(root, 'outputs');
|
||||
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
||||
for (const targets of [historical, [...historical, 'linux-arm64']]) {
|
||||
writeFileSync(source, `export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(targets)};`);
|
||||
writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
|
||||
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const data = Object.fromEntries(readFileSync(output, 'utf8').trim().split('\n').map(line => {
|
||||
const index = line.indexOf('='); return [line.slice(0, index), JSON.parse(line.slice(index + 1))];
|
||||
}));
|
||||
assert.deepEqual(data.matrix.include.map(row => row.target), targets);
|
||||
assert.deepEqual(data.native_consumers.include.map(row => row.target), targets.filter(target => target !== 'linux-x64'));
|
||||
assert.ok(data.matrix.include.every(row => row.target !== 'linux-arm64' || row.host === 'ubuntu-24.04-arm'));
|
||||
}
|
||||
writeFileSync(source, 'export const RELEASE_RUNNER_TARGETS = ["linux-arm64"];'); writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
|
||||
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 1); assert.match(result.stderr, /Unknown exact-source daemon platform contract/);
|
||||
assert.equal(readFileSync(output, 'utf8'), '');
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("historical promotion sources keep their packaging path while new sources require daemon assembly", () => {
|
||||
const workflow = readWorkflow("release.yml");
|
||||
|
||||
@@ -198,7 +198,7 @@ export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevis
|
||||
const { parsePaperclipRunnerdBuildMetadata } = await import(pathToFileURL(join(output, 'evals/runnerd-artifact.js')).href);
|
||||
const { PAPERCLIP_RUNNER_BUILD_METADATA: expected } = await import(pathToFileURL(join(output, 'evals/build-metadata.js')).href);
|
||||
const manifest = JSON.parse(readFileSync(join(output, 'bin/release-manifest.json'), 'utf8'));
|
||||
const targets = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
||||
const targets = ['darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64'];
|
||||
assert.equal(manifest.schema, 'paperclip.runner.release-binaries.v1');
|
||||
assert.equal(manifest.sourceRevision, ${JSON.stringify(sourceRevision)});
|
||||
assert.deepEqual(Object.keys(manifest.platforms).sort(), targets.sort());
|
||||
@@ -240,7 +240,8 @@ export function macInstalledStartupProbeSource(consumerRoot, serverRoot, runnerD
|
||||
serverRoot:${JSON.stringify(serverRoot)},expectedRunnerSha256:${JSON.stringify(runnerDigest.slice(7))}})));`;
|
||||
}
|
||||
|
||||
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMiB = 256, temporaryExecutable = false }) {
|
||||
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMiB = 256, temporaryExecutable = false, platform = 'linux/amd64' }) {
|
||||
if (!['linux/amd64', 'linux/arm64'].includes(platform)) throw new Error('Public-install verification requires a supported native Linux platform');
|
||||
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
|
||||
throw new Error('Public-install verification requires an unprivileged host user');
|
||||
}
|
||||
@@ -251,7 +252,7 @@ export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid,
|
||||
throw new Error('Executable runtime snapshots require an offline verification sandbox');
|
||||
}
|
||||
return [
|
||||
'run', '--rm', '--platform', 'linux/amd64',
|
||||
'run', '--rm', '--platform', platform,
|
||||
'--user', `${uid}:${gid}`, '--read-only',
|
||||
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
||||
'--pids-limit', '256', '--memory', '3g',
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ if [ -n "${PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR:-}" ]; then
|
||||
assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$(git rev-parse HEAD)"
|
||||
cp -R "$REPO_ROOT/packages/paperclip-runner/dist/bin/." "$REPO_ROOT/server/dist/vendor/paperclip-runner/bin/"
|
||||
elif [ "$dry_run" = false ]; then
|
||||
release_fail "Public releases require the three hosted runner daemon artifacts; use the supported Release workflow."
|
||||
release_fail "Public releases require the four hosted runner daemon artifacts; use the supported Release workflow."
|
||||
fi
|
||||
node "$REPO_ROOT/scripts/build-standalone-public-packages.mjs"
|
||||
bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, re
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { stripTypeScriptTypes } from 'node:module';
|
||||
import { basename, dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { packConcurrency, packStagedPackages, packTimings } from './pack-staged-packages.mjs';
|
||||
@@ -95,7 +96,75 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath)
|
||||
console.log(JSON.stringify(receipt));
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
}
|
||||
if (process.argv[2] === '--mac-consumer') {
|
||||
async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptPath) {
|
||||
assert.equal(process.env.GITHUB_ACTIONS, 'true', 'Native clean consumers run only on disposable hosted CI');
|
||||
assert.equal(process.platform + '-' + process.arch, 'linux-arm64', 'ARM qualification requires a native ARM host');
|
||||
assert.match(sourceRevision, /^[a-f0-9]{40}$/);
|
||||
const packages = realpathSync(packageDirectory);
|
||||
const manifest = JSON.parse(readFileSync(join(packages, 'manifest.json'), 'utf8'));
|
||||
assert.equal(manifest.schema, 'paperclip.runner.public-packages/v1');
|
||||
assert.equal(manifest.sourceRevision, sourceRevision);
|
||||
assert.ok(manifest.archives.some(entry => entry.name === '@paperclipai/server'));
|
||||
assert.ok(manifest.archives.some(entry => entry.name === 'paperclipai'));
|
||||
assert.equal(new Set(manifest.archives.map(entry => entry.name)).size, manifest.archives.length);
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'paperclip-arm-public-install-')));
|
||||
try {
|
||||
const assets = join(root, 'assets'), consumer = join(root, 'consumer'), cache = join(root, 'cache');
|
||||
for (const directory of [assets, consumer, cache]) mkdirSync(directory);
|
||||
const run = (command, args, cwd = root) => execFileSync(command, args, { cwd,
|
||||
env: { PATH: process.env.PATH, HOME: root, TMPDIR: root }, timeout: 10 * 60_000,
|
||||
maxBuffer: 32 * 1024 * 1024, encoding: 'utf8' });
|
||||
const platform = 'linux/arm64';
|
||||
run('docker', ['pull', '--platform', platform, GROK_PUBLIC_INSTALL_IMAGE]);
|
||||
const isolated = (command, options = {}) => run('docker', grokConsumerDockerArgs({ assets, consumer, cache,
|
||||
uid: process.getuid(), gid: process.getgid(), platform, command, ...options }));
|
||||
for (const entry of manifest.archives) {
|
||||
assert.match(entry.file, /^[A-Za-z0-9._-]+\.tgz$/); assert.match(entry.sha256, /^[a-f0-9]{64}$/);
|
||||
const tarball = join(packages, entry.file);
|
||||
assert.equal(realpathSync(tarball), tarball);
|
||||
assert.equal(createHash('sha256').update(readFileSync(tarball)).digest('hex'), entry.sha256);
|
||||
assertNoBundledCodexPayloads(run('tar', ['-tzf', tarball]).trim().split('\n'));
|
||||
cpSync(tarball, join(assets, entry.file)); chmodSync(join(assets, entry.file), 0o644);
|
||||
}
|
||||
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' }));
|
||||
const sentinelSource = join(root, 'lifecycle-sentinel'); mkdirSync(sentinelSource);
|
||||
writeFileSync(join(sentinelSource, 'package.json'), JSON.stringify({ name: 'paperclip-verification-lifecycle-sentinel', version: '1.0.0',
|
||||
scripts: { postinstall: 'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'ok\')"' } }));
|
||||
run('npm', ['pack', '--ignore-scripts', '--pack-destination', assets], sentinelSource);
|
||||
isolated(['npm', 'install', '--ignore-scripts', '--omit=dev', ...readdirSync(assets).filter(file => file.endsWith('.tgz')).map(file => `/packages/${file}`)], { download: true });
|
||||
const sentinel = join(consumer, 'node_modules/paperclip-verification-lifecycle-sentinel/lifecycle-ran');
|
||||
assert.equal(existsSync(sentinel), false);
|
||||
const lock = readFileSync(join(consumer, 'package-lock.json'), 'utf8');
|
||||
isolated(GROK_PUBLIC_INSTALL_LIFECYCLE);
|
||||
assert.equal(readFileSync(sentinel, 'utf8'), 'ok');
|
||||
assert.equal(readFileSync(join(consumer, 'package-lock.json'), 'utf8'), lock);
|
||||
for (const entry of manifest.archives) {
|
||||
assert.equal(JSON.parse(readFileSync(join(consumer, 'node_modules', entry.name, 'package.json'))).version, manifest.releaseVersion);
|
||||
}
|
||||
const server = '/consumer/node_modules/@paperclipai/server', installed = server + '/dist/vendor/paperclip-runner';
|
||||
const probe = (name, source, options = {}) => { writeFileSync(join(assets, name), source); return JSON.parse(isolated(['node', '/packages/' + name], options).trim()); };
|
||||
const daemon = probe('daemon-probe.mjs', installedDaemonProbeSource(installed, '/consumer', sourceRevision));
|
||||
assert.equal(daemon.target, 'linux-arm64');
|
||||
const codex = probe('codex-probe.mjs', installedCodexProbeSource(installed + '/index.js', '/consumer', undefined, { nativeCodex: true }));
|
||||
// Reuse the existing zero-turn hook as read-only verifier source. All
|
||||
// production imports still resolve from the installed consumer graph.
|
||||
cpSync(join(repo, 'tests/runner-e2e/installed-cli.ts'), join(assets, 'installed-cli.ts'));
|
||||
writeFileSync(join(assets, 'types.js'), stripTypeScriptTypes(readFileSync(join(repo, 'tests/runner-e2e/types.ts'), 'utf8')));
|
||||
const startup = probe('startup-probe.mjs', macInstalledStartupProbeSource('/consumer', server, daemon.sha256, '/packages/installed-cli.ts'),
|
||||
{ temporarySizeMiB: 2048, temporaryExecutable: true });
|
||||
const receipt = { schema: 'paperclip.runner.linux-arm64-public-install.v1', sourceRevision, platform: process.platform, architecture: process.arch,
|
||||
cleanNpmInstall: true, packageCount: manifest.archives.length, codexPackedPayloadsOmitted: true,
|
||||
consumerImage: GROK_PUBLIC_INSTALL_IMAGE, consumerImagePlatform: platform,
|
||||
consumerLockPreserved: true, lifecycleSentinelVerified: true, lifecycleNetwork: 'none',
|
||||
...daemon, ...codex, startup, providerCalls: 0 };
|
||||
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + '\n');
|
||||
console.log(JSON.stringify(receipt));
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
}
|
||||
if (process.argv[2] === '--linux-arm64-consumer') {
|
||||
assert.equal(process.argv.length, 6, 'Usage: --linux-arm64-consumer PACKAGE_DIRECTORY SOURCE_SHA RECEIPT_PATH');
|
||||
await verifyLinuxArmConsumer(process.argv[3], process.argv[4], process.argv[5]);
|
||||
} else if (process.argv[2] === '--mac-consumer') {
|
||||
assert.equal(process.argv.length, 6, 'Usage: --mac-consumer PACKAGE_DIRECTORY SOURCE_SHA RECEIPT_PATH');
|
||||
await verifyMacConsumer(process.argv[3], process.argv[4], process.argv[5]);
|
||||
} else {
|
||||
|
||||
@@ -170,7 +170,8 @@ describe("adapter routes", () => {
|
||||
[{ driver: "ssh", platform: "linux", architecture: "x64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "darwin", architecture: "arm64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "darwin", architecture: "x64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "linux", architecture: "arm64" }, "legacy"],
|
||||
[{ driver: "ssh", platform: "linux", architecture: "arm64" }, "paperclip"],
|
||||
[{ driver: "ssh", platform: "linux", architecture: "arm" }, "legacy"],
|
||||
[{ driver: "local", platform: "win32", architecture: "x64" }, "legacy"],
|
||||
])("projects the selected environment once for the whole adapter inventory (%s)", async (target, defaultRunner) => {
|
||||
const resolveRunnerTarget = vi.fn(async () => target);
|
||||
|
||||
@@ -267,7 +267,7 @@ describeEmbeddedPostgres("built-in agents", () => {
|
||||
])).toThrow("defaultAdapterType must be allowed");
|
||||
});
|
||||
|
||||
it.each([{ platform: "linux", architecture: "x64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("lazily provisions one agent per company/key and updates the same row on setup ($platform/$architecture)", async ({ platform, architecture }) => {
|
||||
it.each([{ platform: "linux", architecture: "x64" }, { platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("lazily provisions one agent per company/key and updates the same row on setup ($platform/$architecture)", async ({ platform, architecture }) => {
|
||||
runnerTarget.platform = platform;
|
||||
runnerTarget.architecture = architecture;
|
||||
const companyId = await seedCompany();
|
||||
|
||||
@@ -284,7 +284,7 @@ describeEmbeddedPostgres("plugin-managed agents", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([{ platform: "linux", architecture: "x64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("creates managed agents with the most-used compatible company adapter ($platform/$architecture)", async ({ platform, architecture }) => {
|
||||
it.each([{ platform: "linux", architecture: "x64" }, { platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }])("creates managed agents with the most-used compatible company adapter ($platform/$architecture)", async ({ platform, architecture }) => {
|
||||
runnerTarget.platform = platform;
|
||||
runnerTarget.architecture = architecture;
|
||||
const pluginManifest = manifest();
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock("./environment-config.js", () => ({ resolveEnvironmentDriverConfigForRun
|
||||
vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: state.ssh }));
|
||||
import { agentRunnerAvailability, resolveAgentRunnerTargetForCompany, resolveNewAgentRunner, resolveNewAgentRunnerForCompany } from "./agent-runner-selection.js";
|
||||
const target = { driver: "local", platform: "linux", architecture: "x64" };
|
||||
const qualifiedTargets = [target, { driver: "local", platform: "darwin", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "x64" }];
|
||||
const qualifiedTargets = [target, { driver: "local", platform: "linux", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "x64" }];
|
||||
|
||||
describe("server-owned Codex runner selection", () => {
|
||||
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
|
||||
@@ -34,7 +34,7 @@ describe("server-owned Codex runner selection", () => {
|
||||
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn", dangerouslyBypassApprovalsAndSandbox: false } }))
|
||||
.toEqual({ adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false } });
|
||||
});
|
||||
it.each([{ platform: "win32", architecture: "x64" }, { platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "ia32" }])("keeps unqualified $platform/$architecture targets legacy while preserving explicit native profiles", ({ platform, architecture }) => {
|
||||
it.each([{ platform: "win32", architecture: "x64" }, { platform: "linux", architecture: "arm" }, { platform: "darwin", architecture: "ia32" }])("keeps unqualified $platform/$architecture targets legacy while preserving explicit native profiles", ({ platform, architecture }) => {
|
||||
const input = { adapterType: "codex_local", target: { driver: "local", platform, architecture } };
|
||||
expect(resolveNewAgentRunner(input).adapterType).toBe("codex_local");
|
||||
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
||||
@@ -80,13 +80,15 @@ describe("server-owned Codex runner selection", () => {
|
||||
state.ssh.mockResolvedValue({ stdout: "Linux\naarch64\n" });
|
||||
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: null });
|
||||
expect(state.getEnvironment).toHaveBeenCalledWith("default"); expect(state.bindings).toHaveBeenCalledWith("default");
|
||||
expect(result.adapterType).toBe("codex_local");
|
||||
expect(result.adapterType).toBe("paperclip_runner");
|
||||
});
|
||||
it.each([
|
||||
["Linux\nx86_64\n", "paperclip", "paperclip_runner"],
|
||||
["Darwin\narm64\n", "paperclip", "paperclip_runner"],
|
||||
["Darwin\nx86_64\n", "paperclip", "paperclip_runner"],
|
||||
["Linux\naarch64\n", "legacy", "codex_local"],
|
||||
["Linux\naarch64\n", "paperclip", "paperclip_runner"],
|
||||
["Linux\narm64\n", "paperclip", "paperclip_runner"],
|
||||
["Linux\narmv7l\n", "legacy", "codex_local"],
|
||||
])("uses the same selected SSH target for discovery and saving (%s)", async (stdout, expectedRunner, expectedAdapter) => {
|
||||
state.getEnvironment.mockResolvedValue({ id: "selected", driver: "ssh", status: "active", config: {} });
|
||||
state.ssh.mockResolvedValue({ stdout });
|
||||
@@ -121,7 +123,7 @@ describe("Codex creation runner contract", () => {
|
||||
});
|
||||
it("qualifies only the Codex daemon targets shipped in public server packages", () => {
|
||||
for (const { platform, architecture } of qualifiedTargets) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(true);
|
||||
for (const [platform, architecture] of [["darwin", "ia32"], ["linux", "arm64"], ["win32", "x64"], ["win32", "arm64"], ["freebsd", "x64"]]) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(false);
|
||||
for (const [platform, architecture] of [["darwin", "ia32"], ["linux", "arm"], ["win32", "x64"], ["win32", "arm64"], ["freebsd", "x64"]]) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(false);
|
||||
expect(paperclipRunnerSupportsPlatform("claude_local", "darwin", "arm64")).toBe(false);
|
||||
});
|
||||
it("keeps auto out of saved update defaults and validates request intent", () => {
|
||||
|
||||
@@ -41,8 +41,8 @@ function targetPlatform(platform: string, architecture: string) {
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === "win32")("local browser subscription login", () => {
|
||||
it.each([false, true])("uses the selected Codex command with its credential home and ambient HOME/cwd (legacy Linux ARM64: %s)", async (legacy) => {
|
||||
if (legacy) targetPlatform("linux", "arm64");
|
||||
it.each([false, true])("uses the selected Codex command with its credential home and ambient HOME/cwd (legacy Linux ARM32: %s)", async (legacy) => {
|
||||
if (legacy) targetPlatform("linux", "arm");
|
||||
const home = await fakeCli("codex", [
|
||||
'[ "$1 $2" = "login --device-auth" ] || exit 11',
|
||||
'[ "$HOME" = "$CLAUDE_CONFIG_DIR" ] && [ "$HOME" != "$CODEX_HOME" ] && [ "$CODEX_HOME" = "$PAPERCLIP_TEST_LOGIN_CODEX_HOME" ] || exit 12',
|
||||
@@ -66,7 +66,7 @@ describe.skipIf(process.platform === "win32")("local browser subscription login"
|
||||
expect(login.code).toBe("ABCD-EFGHJ");
|
||||
});
|
||||
|
||||
it.each([["linux", "x64"], ["darwin", "arm64"], ["darwin", "x64"]] as const)("reports an unavailable Codex CLI on %s/%s without revealing private details", async (platform, architecture) => {
|
||||
it.each([["linux", "x64"], ["linux", "arm64"], ["darwin", "arm64"], ["darwin", "x64"]] as const)("reports an unavailable Codex CLI on %s/%s without revealing private details", async (platform, architecture) => {
|
||||
targetPlatform(platform, architecture);
|
||||
const home = await fakeCli("codex", 'echo invoked > "$CODEX_HOME/unreviewed-cli"');
|
||||
const failure = new Error("private-installation-path-or-provider-output");
|
||||
@@ -79,8 +79,8 @@ describe.skipIf(process.platform === "win32")("local browser subscription login"
|
||||
expect(login.authorizationUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reports a missing Linux ARM64 Codex CLI without revealing the host PATH", async () => {
|
||||
targetPlatform("linux", "arm64");
|
||||
it("reports a missing Linux ARM32 Codex CLI without revealing the host PATH", async () => {
|
||||
targetPlatform("linux", "arm");
|
||||
const home = await fakeCli("codex", "exit 0");
|
||||
process.env.PATH = "/usr/bin:/bin";
|
||||
const login = startLocalBrowserLogin("openai", home);
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { runSetupTokenLogin } from "@paperclipai/adapter-claude-local/server";
|
||||
import { runDeviceLogin } from "@paperclipai/adapter-codex-local/server";
|
||||
import { ensureCommandResolvable, resolveCommandForLogs } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { paperclipRunnerSupportsPlatform } from "@paperclipai/shared";
|
||||
import { resolveCodexCommand } from "../vendor/paperclip-runner/index.js";
|
||||
|
||||
export type LocalBrowserLoginState = {
|
||||
@@ -56,10 +57,9 @@ export function startLocalBrowserLogin(provider: "anthropic" | "openai", home: s
|
||||
async start(_command: string, onData: (chunk: string) => void): Promise<{ exitCode: number | null }> {
|
||||
let executable = "claude";
|
||||
if (provider === "openai") {
|
||||
// Linux ARM64 retains its supported legacy CLI path; it is not a
|
||||
// qualified native runner target. Other hosts prefer the installed
|
||||
// dependency, then an existing CLI when the dependency is absent.
|
||||
const legacyPlatform = process.platform === "linux" && process.arch === "arm64";
|
||||
// Native targets use the same owned Codex resolution as execution.
|
||||
// Other platforms retain the legacy CLI lookup.
|
||||
const legacyPlatform = !paperclipRunnerSupportsPlatform("codex_local", process.platform, process.arch);
|
||||
try {
|
||||
if (legacyPlatform) {
|
||||
await ensureCommandResolvable("codex", home, process.env);
|
||||
|
||||
@@ -1221,6 +1221,14 @@ describe("CompanyImport", () => {
|
||||
{ sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: { codexPermissionMode: "never", dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: false }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true }, expectedBypass: true },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["--sandbox", "read-only"] }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { args: ["-c", "approval_policy=on-request"] }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: [], args: ["--profile", "restricted"] }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["--color", "never"], args: ["--sandbox", "read-only"] }, expectedBypass: true },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["-c", "sandbox_workspace_write.network_access=false"] }, expectedBypass: false },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true, extraArgs: ["--sandbox", "read-only"] }, expectedBypass: true },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: true, args: ["--sandbox", "read-only"] }, expectedBypass: true },
|
||||
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true, extraArgs: ["--color", "never"] }, expectedBypass: false },
|
||||
])("submits $runner from $sourceType with visible bypass=$expectedBypass while preserving saved policy", async ({ sourceType, sourceRunner, runner, sourcePolicy, expectedBypass }) => {
|
||||
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
|
||||
mockAdaptersApi.list.mockResolvedValue([
|
||||
|
||||
@@ -43,7 +43,7 @@ import { getAdapterLabel } from "../adapters/adapter-display-registry";
|
||||
import { defaultCreateValues } from "../components/agent-config-defaults";
|
||||
import { getUIAdapter, listUIAdapters } from "../adapters";
|
||||
import { isPaperclipRunnerProvider, PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES, type CreateConfigValues } from "@paperclipai/adapter-utils";
|
||||
import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local";
|
||||
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
|
||||
import {
|
||||
type FileTreeNode,
|
||||
type FrontmatterData,
|
||||
@@ -589,7 +589,7 @@ function importAdapterValues(
|
||||
dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass
|
||||
: nativeCodex
|
||||
? (sourceConfig.codexPermissionMode ?? PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES.codex.defaultMode) === "never"
|
||||
: DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
: resolveCodexLocalCliPolicy(sourceConfig).bypass,
|
||||
} : {}),
|
||||
...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot"
|
||||
? { adapterSchemaValues: { ...sourceConfig } } : {}),
|
||||
|
||||
Reference in new issue
Block a user