mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
feat(runner): activate qualified OpenCode and ACPX providers (#12691)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Runner is the experimental native runtime for governed agent work. > - The runtime contracts already describe Codex, OpenCode, and ACPX providers. > - The merged control plane still rejected OpenCode and ACPX for new runner agents. > - Runnerd also selected only the Codex provider implementation. > - This pull request activates the qualified OpenCode and ACPX paths from the form to runnerd. > - The benefit is one durable runner path with provider-specific permissions and recovery. ## Linked Issues or Issue Description Refs #12685 **Subsystem affected** This change affects the runner package, server orchestration, adapter configuration, and UI configuration. **Problem or motivation** Paperclip Runner stores provider contracts for OpenCode and ACPX. New agents cannot select those providers. Runnerd cannot execute those stored provider descriptors. The UI also shows only Codex. **Proposed solution** Accept the qualified OpenCode 1.18.17 profile and the fixed ACPX Claude and Codex profiles. Route them through runnerd. Keep provider selection, model selection, permissions, credentials, events, and recovery inside closed provider-specific boundaries. **Alternatives considered** One option was to keep the contracts dormant. That option leaves stored configuration and runtime behavior out of sync. Another option was to enable every ACPX agent. That option is not safe because Pi does not yet have the same verified launch path. **Roadmap alignment** This change supports the completed cloud and sandbox agent milestone. It also supports self-healing runs and governed agent execution. It does not add a new roadmap surface. ## What Changed - Add one server profile resolver for Codex, OpenCode, and qualified ACPX descriptors. - Keep `adapterConfig` as the provider and permission authority for fresh runs. - Add Paperclip Runner provider, ACPX agent, and provider-specific permission controls to the UI. - Reset the model to a compatible qualified value when the provider changes. - Route Codex, OpenCode, and ACPX through the durable runnerd provider selector. - Add a durable ACPX executor with bounded state, recovery, events, tool receipts, and identity checks. - Remove Codex labels from OpenCode events, results, evidence, and recovery diagnostics. - Pass only provider-specific credential names to child processes. - Keep ACPX Pi unavailable and reject it before process launch. - Keep the existing Paperclip Runner experimental flag unchanged. ## Verification - `pnpm exec vitest run packages/paperclip-runner/src/backends/native-backend-factory.test.ts packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts packages/adapters/codex-local/src/ui/build-config.test.ts ui/src/adapters/codex-local/config-fields.test.tsx server/src/__tests__/adapter-registry.test.ts server/src/__tests__/adapter-routes.test.ts server/src/__tests__/agent-adapter-validation-routes.test.ts server/src/__tests__/company-portability.test.ts server/src/services/native-runtime/runtime-mode.test.ts server/src/services/native-runtime/native-session-executor.test.ts server/src/services/heartbeat-runner-provider-config.test.ts` - The focused TypeScript, server, and UI suites passed 274 tests. - `cargo test -p paperclip-runner-core --test native_provider_backend` - The executable native provider integration suite passed 4 tests. - `cargo test -p paperclip-runner-core --lib` - The Rust unit suite passed 91 tests. - `pnpm -r typecheck` - `pnpm check:token-gates` - `pnpm build` - `git diff --check codex/runner-parity-task-runtime...HEAD` ## Risks - This changes provider process selection and durable recovery. The experimental flag still gates every fresh Paperclip Runner run. - OpenCode requires a model in `provider/model` form and stays pinned to version 1.18.17. - ACPX accepts only exact Claude and Codex profile versions and models. Pi stays unavailable. - ACPX steering stays unavailable and reports that limit through the driver capabilities. - Child processes receive explicit environment allowlists. They do not inherit the full server environment. - This pull request has no database migration. ## Model Used OpenAI Codex, GPT-5, with tool use, code execution, and subagent review. ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
1 parent
72b9f92d76
commit
84bedd4ca1
37 files changed
+3195
-286
No files matched your search
@@ -128,7 +128,7 @@ describe("buildPaperclipRunnerConfig", () => {
|
||||
it("fails closed to the Codex profile and safe defaults for stale schema values", () => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
adapterSchemaValues: {
|
||||
provider: "opencode",
|
||||
provider: "unknown",
|
||||
codexPermissionMode: "unrestricted",
|
||||
lifecycleMode: "forever",
|
||||
idleTimeoutMs: -1,
|
||||
@@ -140,6 +140,74 @@ describe("buildPaperclipRunnerConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("builds a qualified OpenCode profile from schema-backed values", () => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
adapterType: "paperclip_runner",
|
||||
model: "",
|
||||
adapterSchemaValues: {
|
||||
provider: "opencode",
|
||||
opencodePermissionMode: "allow",
|
||||
},
|
||||
}))).toMatchObject({
|
||||
provider: "opencode",
|
||||
model: "openrouter/deepseek/deepseek-v4-flash-0731",
|
||||
opencodePermissionMode: "allow",
|
||||
codexPermissionMode: "untrusted",
|
||||
acpxPermissionMode: "approve-reads",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let a stale schema model override the active Codex model", () => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
adapterType: "paperclip_runner",
|
||||
model: "gpt-5.6-sol",
|
||||
adapterSchemaValues: {
|
||||
provider: "codex",
|
||||
model: "openrouter/stale-model",
|
||||
codexPermissionMode: "on-request",
|
||||
},
|
||||
}))).toMatchObject({
|
||||
provider: "codex",
|
||||
model: "gpt-5.6-sol",
|
||||
codexPermissionMode: "on-request",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude", "claude-sonnet-5"],
|
||||
["codex", "gpt-5.6-sol"],
|
||||
] as const)("builds the qualified ACPX %s profile", (acpxAgent, model) => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
adapterType: "paperclip_runner",
|
||||
model: "stale-model-from-another-provider",
|
||||
adapterSchemaValues: {
|
||||
provider: "acpx",
|
||||
acpxAgent,
|
||||
acpxPermissionMode: "approve-all",
|
||||
},
|
||||
}))).toMatchObject({
|
||||
provider: "acpx",
|
||||
acpxAgent,
|
||||
model,
|
||||
acpxPermissionMode: "approve-all",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not materialize the unavailable ACPX Pi profile", () => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
adapterType: "paperclip_runner",
|
||||
model: "",
|
||||
adapterSchemaValues: {
|
||||
provider: "acpx",
|
||||
acpxAgent: "pi",
|
||||
},
|
||||
}))).toMatchObject({
|
||||
provider: "acpx",
|
||||
acpxAgent: "claude",
|
||||
model: "claude-sonnet-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds warm lifecycle values to the shared safe default", () => {
|
||||
expect(buildPaperclipRunnerConfig(makeValues({
|
||||
paperclipRunnerLifecycleMode: "warm",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
buildAdapterEnvConfig,
|
||||
isPaperclipRunnerProvider,
|
||||
resolvePaperclipRunnerIdleTimeoutMs,
|
||||
resolvePaperclipRunnerPermissionMode,
|
||||
type CreateConfigValues,
|
||||
@@ -69,6 +70,7 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unk
|
||||
/** Build a provider profile accepted by the experimental Rust runner. */
|
||||
export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const config = buildCodexLocalConfig(v);
|
||||
const schemaValues = { ...(v.adapterSchemaValues ?? {}) };
|
||||
for (const unsupportedKey of [
|
||||
"engine",
|
||||
"agentCommand",
|
||||
@@ -86,8 +88,19 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
|
||||
"extraArgs",
|
||||
]) {
|
||||
delete config[unsupportedKey];
|
||||
delete schemaValues[unsupportedKey];
|
||||
}
|
||||
const schemaValues = v.adapterSchemaValues ?? {};
|
||||
const providerCandidate = schemaValues.provider;
|
||||
const provider = isPaperclipRunnerProvider(providerCandidate)
|
||||
? providerCandidate
|
||||
: "codex";
|
||||
const acpxAgent = schemaValues.acpxAgent === "codex" ? "codex" : "claude";
|
||||
const schemaModel = typeof schemaValues.model === "string"
|
||||
? schemaValues.model.trim()
|
||||
: "";
|
||||
const configuredModel = typeof config.model === "string"
|
||||
? config.model.trim()
|
||||
: "";
|
||||
const lifecycleCandidate = v.paperclipRunnerLifecycleMode ?? schemaValues.lifecycleMode;
|
||||
const lifecycleMode = lifecycleCandidate === "warm" ? "warm" : "per_turn";
|
||||
const configuredIdleTimeoutMs =
|
||||
@@ -95,13 +108,47 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
|
||||
const idleTimeoutMs = resolvePaperclipRunnerIdleTimeoutMs(
|
||||
configuredIdleTimeoutMs,
|
||||
);
|
||||
for (const normalizedKey of [
|
||||
"provider",
|
||||
"model",
|
||||
"acpxAgent",
|
||||
"codexPermissionMode",
|
||||
"opencodePermissionMode",
|
||||
"acpxPermissionMode",
|
||||
"lifecycleMode",
|
||||
"idleTimeoutMs",
|
||||
]) {
|
||||
delete schemaValues[normalizedKey];
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
provider: "codex",
|
||||
...schemaValues,
|
||||
provider,
|
||||
codexPermissionMode: resolvePaperclipRunnerPermissionMode(
|
||||
"codex",
|
||||
v.codexPermissionMode ?? schemaValues.codexPermissionMode,
|
||||
v.adapterSchemaValues?.codexPermissionMode ?? v.codexPermissionMode,
|
||||
),
|
||||
opencodePermissionMode: resolvePaperclipRunnerPermissionMode(
|
||||
"opencode",
|
||||
v.adapterSchemaValues?.opencodePermissionMode,
|
||||
),
|
||||
acpxPermissionMode: resolvePaperclipRunnerPermissionMode(
|
||||
"acpx",
|
||||
v.adapterSchemaValues?.acpxPermissionMode,
|
||||
),
|
||||
...(provider === "opencode"
|
||||
? {
|
||||
model: schemaModel
|
||||
|| configuredModel
|
||||
|| "openrouter/deepseek/deepseek-v4-flash-0731",
|
||||
}
|
||||
: {}),
|
||||
...(provider === "acpx"
|
||||
? {
|
||||
acpxAgent,
|
||||
model: acpxAgent === "claude" ? "claude-sonnet-5" : "gpt-5.6-sol",
|
||||
}
|
||||
: {}),
|
||||
lifecycleMode,
|
||||
...(lifecycleMode === "warm" ? { idleTimeoutMs } : {}),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user