Files
PaperClipAI/packages/paperclip-runner/scripts/copilot-profile-identity.mjs
T
DottaandPaperclip 734c265339 Default new Pi agents to Paperclip Runner (#17142)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Users choose a harness during agent setup.
> - Claude, Codex, and OpenCode already receive their qualified Runner
default.
> - Pi has a qualified native integration but new Pi agents still use
the direct adapter.
> - This change gives new Pi agents the native default on supported
targets.
> - Saved agents keep their runner and compatible settings. Legacy
remains available in Advanced.

## Linked Issues or Issue Description

Refs #17120, #14921, #14922, #14923, #14924, and #14956. Related generic
default work: #15422. This PR is the separate Pi follow-up to the merged
OpenCode change.

**Current behavior**

Choosing Pi for a new agent saves the direct Pi adapter. The native Pi
integration remains behind the generic experimental flag.

**Proposed behavior**

Default new Pi agents to ACPX Pi on macOS ARM64/x64 and GNU/Linux x64
with supported glibc. Keep unsupported targets and explicit Legacy
selections on the direct adapter. Require an explicit provider/model
before test or hire. Permit model-free inactive setup drafts.

**Reason and benefit**

Users choose Pi once and receive its qualified runner. Saved agents
retain their identity and settings. Conversions reject unsupported
legacy thinking and launch settings instead of dropping them.

## What Changed

- Add Pi to the shared platform/default contract and server creation
selection.
- Admit native Pi without the generic experimental flag.
- Preserve provider models, credentials, workspace settings, exact
thinking, and saved native policies.
- Add Pi runner controls, branding, setup resume, onboarding, built-in
configuration, and import handling.
- Add managed Pi OpenRouter routing and matching native provider API-key
accounts. Keep subscription and custom gateway exclusions. Clear
alternate credentials and reject custom Pi provider overrides.
- Keep Pi draft models empty instead of applying Claude's default model.
- Verify setup against the pinned Pi runtime. For native Pi on the
official OpenRouter route, reverify the API key live without requiring a
global Legacy Pi CLI.
- Match Pi’s bounded controller close window to its native snapshot
cleanup and lifetime verification, preserving explicit caller deadlines
and the mandatory drain/suspension proof.
- Bind Pi sidecars to the package that owns the pinned native assets,
including source and vendored server layouts. Other providers retain
their existing package authority.
- Keep protocol-owned finish/block tools out of the ACPX forwarded
catalog. The trusted bridge installs these tools itself; the complete
Rust authorization catalog and terminal receipt ledger remain intact.
This fixes actual Pi session startup and applies to other ACPX
consumers.
- Advance Copilot’s shared-source attestation to v58 because it pins the
shared files touched by Pi. Preserve immutable v57 bytes and historical
decoding; Pi’s own release identity is unchanged.
- Add regression coverage and update the paired maps and developer
documentation.

## Verification

- Passed: full repository typecheck and full production build on final
head 656d304783, including the generated attestation repair.
- Passed: focused shared/server/UI suites covering default selection,
platform fallback, conversion preservation, inactive drafts, managed
account compatibility, credential isolation, OpenRouter routing/model
IDs, exact thinking controls, and saved-account selection. Relevant
batches include 375 server/conversion tests, 267 account/routing tests,
and 110 UI setup/account tests.
- Passed: 179 attestation, historical-profile, qualified-profile, and
native-execution contract tests after CI identified stale Copilot source
hashes. The identity generator check passes; the only delta after the
passing Pi browser run is this Copilot attestation/history repair.
- Passed: 145 managed-account tests, including actual service
preparation and API-key adoption/rejection for native Pi without the
Legacy CLI; 6 pinned-installation probe tests.
- Red then green: an actual task failed session.open because its
54-operation catalog included the bridge-owned terminal tools. The
regression with those tools failed before the filter; all 399 Rust
library tests pass after it. The full product retry exposed an
additional package-authority mismatch. Four authority tests failed
before that repair; 11 authority checks and 14 installation/asset checks
pass afterward. Direct real Pi session initialization with the corrected
package root and catalog succeeds. No model prompt was sent by that
diagnostic.
- Red then green: real Pi posted Hello from Pi but its controller closed
before native cleanup could finish. The cleanup-budget test failed with
the old default and passes with the bounded Pi window. All 46 selected
suspension, drain, and package-authority tests pass after building their
native fixture.
- Passed: token gates, paired map checks, Rust formatting, and git diff
--check.
- Passed real browser acceptance on macOS ARM64, generic native runner
flag disabled, source a433748a12 (Pi code unchanged afterward): the UI
selects Paperclip Runner by default, retains Pi and the selected
model/account, and verifies the pinned runtime plus live OpenRouter key.
A fresh UI-created task, assigned to the UI-created native Pi agent,
posted exactly `Hello from Pi` and reached Done without retry or
restart. The native run and workspace finalization succeeded; the
response and Done persisted after Reload.
- Evidence: 8 preserved browser attempts, including the final passing
18-step journey with all screenshots. Prior failures remain recorded.
The earlier failed checkpoint run recovered on server restart; that
recovery is separate from the fresh final task’s clean success.
- Preserved browser failures: missing account control/inherited Claude
draft model; incompatible managed-account/model-prefix handling; generic
npm probe for Pi; Legacy CLI hello check; duplicate terminal tools and
wrong provider-package authority at actual native session.open; a real
Hello from Pi comment followed by premature checkpoint cleanup timeout.
Each failure has retained screenshots/logs and a corresponding focused
repair. The fresh final task proves these repairs end to end.
- The exact Runner Vitest shard was also run locally after the
attestation fix: the stale-hash failure is gone; 1,833 tests passed, 10
skipped, and two Codex retained-control settlement scenarios failed with
`native_cleanup_maintenance_unproven` during terminal ACK/suspension
replay. Both scenarios passed in an isolated rerun (2/2), and the same
19-test file passed in the preceding Linux CI run. The precise
full-shard timing cause is not established. These Codex scenarios are
outside the changed Pi paths; the original local failures are retained;
no complete local Runner-shard pass is claimed. The corresponding Runner
shard now passes on the final head in hosted Linux CI. All 60 final-head
check runs are now terminal and clean (55 successful, 1 neutral, 4
skipped), and the legacy Snyk status is successful.
- Local full repository tests were attempted but not completed:
provisioning briefly confused a workspace symlink, then a later run was
stopped before browser-driven repairs. No full local-suite pass is
claimed. Final-head hosted CI is green and fresh Greptile review is 5/5
with all review threads resolved; earlier jobs canceled by runner
shutdown are not counted as a pass.

## Risks

- Pi cleanup has a finite 45-second default window, accounting for the
native 30-second snapshot cleanup and 5-second lifetime fence. Other
providers and explicit caller deadlines retain their prior behavior.
- Pi requires explicit operator setup with `paperclipai runtime setup
pi` on the execution host. This PR does not add automatic downloads.
- Linux ARM64, Windows, musl, and unqualified targets retain Legacy.
- Legacy thinking levels minimal, medium, and xhigh cannot be translated
exactly. Native max thinking, warm lifecycle, or restricted permissions
cannot be dropped during a return to Legacy.
- Copilot’s bound profile advances to v58 with exact current launch
admission; v57 remains decodable for recovery. No historical identity is
rewritten.
- Existing agents and immutable run descriptors are not migrated. Cloud
Pi availability is unchanged.

## Model Used

OpenAI GPT-6 through Codex with reasoning, code editing, command
execution, and browser use. The exact serving model ID and
context-window size are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-21 17:10:00 -05:00

111 lines
7.4 KiB
JavaScript

// Regenerate after changing any bound implementation; live qualification must
// restart whenever the resulting identity changes. Historical identities remain.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v58-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 58 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
displayProjectionSourceSha256: "src/drivers/acpx/copilot-extension-adapter.ts",
configuredEnvironmentSourceSha256: "src/configured-environment.ts",
configuredEnvironmentRustSourceSha256: "runner/crates/runner-core/src/configured_environment.rs",
profileActivitySourceSha256: "src/drivers/acpx/profile-activity.ts",
profileExtensionsSourceSha256: "src/drivers/acpx/profile-extensions.ts",
profileInstallationSourceSha256: "src/drivers/acpx/profile-installation.ts",
attachmentPolicySourceSha256: "runner/crates/runner-core/src/acpx_provider_capabilities.rs",
singleReadEvidenceSourceSha256: "src/drivers/acpx/single-read-evidence.ts",
nativeSessionRuntimeSourceSha256: "src/native-session-runtime.ts",
nativeRuntimeContextSourceSha256: "src/contracts/runtime-context.ts",
nativeRuntimePromptsSourceSha256: "src/contracts/runtime-context-prompts.ts",
nativeSessionContractSourceSha256: "src/contracts/native-session-backend.ts",
harnessSessionContractSourceSha256: "src/contracts/harness-driver.ts",
harnessBackendSourceSha256: "src/backends/harness-driver-backend.ts",
harnessSessionSourceSha256: "src/drivers/codex/codex-harness-session.ts",
transportContractSourceSha256: "src/drivers/codex/app-server-transport.ts",
nativeBackendSourceSha256: "src/backends/codex-native-backend.ts",
nativeBackendFactorySourceSha256: "src/backends/native-backend-factory.ts",
nativeInstructionsSourceSha256: "src/backends/runtime-context.ts",
providerLaunchSourceSha256: "src/live/runner-provider-launch.ts",
launchPreparationStoreSourceSha256: "src/live/runner-launch-preparation-store.ts",
runtimeRequestBridgeSourceSha256: "src/live/runnerd-codex-transport.ts",
runtimeRequestFacadeSourceSha256: "src/drivers/codex/codex-session-notifications.ts",
runtimeRequestNotificationSourceSha256: "src/drivers/codex/codex-thread-normalization.ts",
guardedInstallationSourceSha256: "src/drivers/acpx/installation-integrity.ts",
nativeExecutionSourceSha256: "src/contracts/native-execution.ts",
profileCompatibilitySourceSha256: "src/drivers/acpx/profile-compatibility.ts",
installationSourceSha256: "src/drivers/acpx/copilot-installation.ts",
assetOwnershipSourceSha256: "src/drivers/acpx/provider-assets-root.ts",
assetInstallerSourceSha256: "scripts/install-copilot-assets.mjs",
credentialBindingSourceSha256: "src/drivers/acpx/environment.ts",
sidecarErrorSourceSha256: "src/cli/acpx-sidecar-input.ts",
profileSourceSha256: "src/drivers/acpx/copilot-profile.ts",
metadataProbeSourceSha256: "src/drivers/copilot-metadata-probe.ts",
acpxPatchSha256: "../../patches/acpx@0.13.1.patch",
policySha256: "src/drivers/acpx/copilot-policy.ts",
distributionSourceSha256: "scripts/materialize-copilot-binary.mjs",
innerDistributionSourceSha256: "scripts/copilot-inner-distribution.mjs",
permissionContextSourceSha256: "src/drivers/acpx/copilot-permission-context.ts",
permissionAdapterSourceSha256: "src/drivers/acpx/acp-permission-adapter.ts",
permissionToolIdentitySourceSha256: "src/drivers/acpx/cursor-plan-tool-identity.ts",
permissionLocationsSourceSha256: "src/drivers/acpx/safe-locations.ts",
permissionRedactionSourceSha256: "src/semantic-tools/redaction.ts",
permissionClassifierSourceSha256: "src/drivers/acpx/generated-sidecar-contract.ts",
semanticReceiptSourceSha256: "src/drivers/semantic-tool-receipt.ts",
semanticBridgeSourceSha256: "src/drivers/runner-tool-bridge.ts",
toolEvidenceSourceSha256: "src/drivers/acpx/copilot-tool-evidence.ts",
semanticSidecarSourceSha256: "src/cli/acpx-runtime-sidecar.ts",
semanticContinuationSourceSha256: "src/drivers/completion-continuation.ts",
semanticDirectDriverSourceSha256: "src/drivers/acpx/codex-acpx-driver.ts",
semanticValidationSourceSha256: "src/protocol/replay-contract.ts",
semanticNormalizationSourceSha256: "src/protocol/result-normalization.ts",
semanticCompletionContractSourceSha256: "src/contracts/completion-result.ts",
semanticValidatorsSourceSha256: "src/protocol/generated/standalone-validators.ts",
semanticSchemaBundleSourceSha256: "src/protocol/generated/schema-bundle.ts",
semanticSidecarProtocolSourceSha256: "src/drivers/acpx/sidecar-protocol.ts",
runtimeHostSourceSha256: "src/drivers/acpx/runtime-host.ts",
runtimeAdapterSourceSha256: "src/drivers/acpx/codex-runtime-adapter.ts",
runtimeSandboxSourceSha256: "src/drivers/acpx/runtime-sandbox.ts",
cancellationSourceSha256: "src/drivers/acpx/turn-cancellation.ts",
agentFilesSourceSha256: "src/drivers/acpx/agent-files-binding.ts",
nativeDistributionSourceSha256: "src/drivers/acpx/native-distribution-integrity.ts",
};
for (const [field, path] of Object.entries(sources)) {
declaration[field] = createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex");
}
const sorted = Object.fromEntries(Object.entries(declaration).sort(([a], [b]) => a.localeCompare(b)));
const commandDigest = `sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`;
const identity = `${JSON.stringify({ commandDigest, declaration }, null, 2)}\n`;
// The shared manifest now generates both TypeScript and Rust admission. Update
// only Copilot; other providers retain their upstream release attestations.
const manifestPath = resolve(root, "acpx-profiles.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (process.argv.includes("--check")) {
if (manifest.profiles.copilot.commandDigest !== commandDigest
|| manifest.profiles.copilot.agentProfileVersion !== declaration.agentProfileVersion) {
throw new Error("Copilot identity is stale in acpx-profiles.json");
}
} else {
manifest.profiles.copilot.commandDigest = commandDigest;
manifest.profiles.copilot.agentProfileVersion = declaration.agentProfileVersion;
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}
// The builder reads the active manifest; no duplicate digest literal is allowed.
const distributionSource = readFileSync(resolve(root, "scripts/build-copilot-distribution.mjs"), "utf8");
if (!distributionSource.includes("profiles.profiles.copilot.commandDigest")) {
throw new Error("Copilot distribution builder must use the active manifest identity");
}
// The shared generator validates this exact attestation before emitting admission.
if (process.argv.includes("--check")) {
if (readFileSync(fixture, "utf8") !== identity) throw new Error("Copilot source identity is stale");
} else writeFileSync(fixture, identity);
execFileSync(process.execPath, [resolve(root, "scripts/generate-acpx-profiles.mjs"),
...(process.argv.includes("--check") ? ["--check"] : [])], { stdio: "inherit" });
console.log(commandDigest);