mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
merge: reconcile latest Codex runner target qualification
Preserve the user-approved Codex Linux ARM64 default and keep Claude automatic selection limited to Apple Silicon. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
5f0b898e9e
25 files changed
+371
-99
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
|
||||
|
||||
@@ -4,7 +4,7 @@ Updated 2026-10-12.
|
||||
|
||||
## Outcome and constraints
|
||||
|
||||
Start from `058af8f1b69c5fa3d9bc974b9eab73c141b52823`; branch `codex/claude-runner-default`, checkout `/Users/dotta/.codex/worktrees/670b/paperclip`. New Claude agents eventually default to native ACPX Claude on Apple Silicon, Intel Mac, Linux x64 and Linux ARM64. Codex ARM64 defaults remain unchanged. Ordinary Claude defaults become Opus 5.5; explicit models and existing runner selections remain intact.
|
||||
Start from `058af8f1b69c5fa3d9bc974b9eab73c141b52823`; branch `codex/claude-runner-default`, checkout `/Users/dotta/.codex/worktrees/670b/paperclip`. New Claude agents eventually default to native ACPX Claude on Apple Silicon, Intel Mac, Linux x64 and Linux ARM64. Codex selection remains owned by the dependency branch. On 2026-10-12 the user explicitly approved preserving its latest Linux ARM64 default from `dcdf09a0f33536fa2b6daaf6e731d921d5711282` during reconciliation. Ordinary Claude defaults become Opus 5.5; explicit models and existing runner selections remain intact.
|
||||
|
||||
Local Apple Silicon API-key task and follow-up are the first gate. Keep existing connection/login tools. Complete core settings and local subscription next, then package/qualify other targets. Two reviewable PRs; no merge or deployment. Linux ARM64 covers installed hosts and SSH, not ARM cloud images. Separate $50 Claude ceiling including setup probes, unknown-charge holds and task turns. No automatic paid retry loops. Docker and expensive Rust builds run in cloud infrastructure; never build Docker images on this Mac.
|
||||
|
||||
@@ -44,3 +44,10 @@ Follow-up run `60e7f9a2-aff3-40b3-8493-2ede6c59da72` succeeded after a controlle
|
||||
Product evidence: [task and both downloadable artifacts](http://127.0.0.1:3105/CLA/issues/CLA-1), [first artifact](http://127.0.0.1:3105/api/attachments/3b1b6033-78b4-4a0b-99ae-de363d7f5071/content?download=1), [revision](http://127.0.0.1:3105/api/attachments/857add4a-ef8c-4146-b9c5-be9affc32565/content?download=1). Sanitized receipt: `/private/tmp/claude-local-api-acceptance.json`.
|
||||
|
||||
Runtime: daemon source `d522822a5daeb72d124360b576b82a8bb52b43f4`, executable sha256 `d0656673f7b6dfa6f2554fac18f3e0b42e2eb10f444b06f6063255335d71aa14`; SDK CLI 2.1.286 sha256 `75e3016e9d2570767b08e43a7467d4817a4f149232c169ca295f2c95fef21433`. The planned effort transport changes require a new remotely built daemon and affected acceptance reruns; they are not covered by this baseline-runtime evidence.
|
||||
|
||||
|
||||
## Behavior candidate and cloud checks
|
||||
|
||||
Implementation commit `6fd56c445d` adds the Apple Silicon default, native-input v7 effort admission, compatible settings/preservation, existing authentication routing, and Opus API pricing. Historical v1-v6 readers and Codex/Pi admission remain covered. Focused runner tests passed (197 tests before the additional three admission/recovery tests; the new focused admission/recovery suite also passed 111 tests). Focused server selection/provider projection, UI/import, routing and token gates passed; two stale experimental-gate fixtures were corrected and the New Agent suite then passed 60 tests. Updated server/runner/UI TypeScript checks passed. Required broad checks and the updated daemon run in cloud CI.
|
||||
|
||||
Reconcile the latest Codex dependency (`dcdf09a0f3`, direct descendant of the requested baseline), preserve its newly supplied four-target producer/consumer work, and keep Claude automatic selection restricted to Apple Silicon. No Docker build ran locally. The baseline local API proof remains tied to its original runtime; new effort acceptance requires the cloud-built candidate.
|
||||
@@ -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);
|
||||
|
||||
@@ -14,12 +14,12 @@ export function paperclipRunnerProfileForHarness(harness: string): { provider: "
|
||||
return 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 {
|
||||
// Claude graduates on Apple Silicon first; other targets await package and live qualification.
|
||||
if (harness === "claude_local") return platform === "darwin" && architecture === "arm64";
|
||||
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 });
|
||||
@@ -122,7 +124,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(true);
|
||||
});
|
||||
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,
|
||||
@@ -590,7 +590,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,
|
||||
} : {}),
|
||||
...(sourceConfig && choice === "claude_local" ? { model: typeof sourceConfig.model === "string" ? sourceConfig.model : "", thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? sourceConfig.effort ?? "") } : {}),
|
||||
...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot"
|
||||
|
||||
Reference in new issue
Block a user