feat(runner): add administration and observability (#12641)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Administrators need bounded controls for experimental native
execution.
> - The lower stack adds remote Codex execution and the task workspace.
> - Operators need to configure Codex safely and inspect provider
traces.
> - Unsupported providers must not appear as runnable choices.
> - This pull request adds Codex-only administration and observability.
> - The benefit is a default-off operational surface for production
diagnosis.

## Linked Issues or Issue Description

Refs #12640.
Refs #12616.
Refs #12352.

**Subsystem affected**

Agent configuration, instance experimental settings, run ledger,
provider trace inspector, and administrator actions.

**Problem or motivation**

The native runner lacks one safe operator surface for Codex permissions,
lifecycle, raw trace capture, and run inspection. The integration branch
also contains provider choices that the production backend cannot
execute yet.

**Proposed solution**

Expose only the qualified Codex controls. Keep Paperclip Developer Mode
and runner preview ingress off by default. Gate raw trace actions by
administrator access and existing trace authorization.

**Alternatives considered**

Exposing unfinished providers would create configurations that fail at
runtime. Always-on tracing would increase sensitive data and storage
risk.

**Roadmap alignment**

This work supports governed Cloud and Sandbox agents and production
diagnostics.

## Stack

- Base PR: #12640.
- Lower PRs: #12639 and #12638.
- This PR contains only its 54-file administration and observability
delta.
- This is the final feature PR in the Codex production stack.

## What Changed

- Added Codex-only Paperclip Runner permission and lifecycle controls.
- Added bounded warm idle configuration.
- Kept the provider field fixed to Codex.
- Added administrator-only one-run raw trace requests.
- Added a persistent future-run raw trace toggle.
- Added trace status, metadata, ledger, and canonical runner inspection.
- Added JSON-RPC request-origin grouping and finalization lineage.
- Restored the stateful PRP transcript parser and focused projection
tests required by trace inspection.
- Added default-off Paperclip Developer Mode.
- Added Honeycomb run links for authorized developer mode.
- Disabled the legacy operational skill for `paperclip_runner`.
- Did not expose OpenCode, ACPX, Pi, Claude Managed, or AWS runner
choices.
- Did not change migrations, workflows, dependencies, or
`pnpm-lock.yaml`.

## Verification

- GitHub Actions will run UI tests, server tests, repository typecheck,
build, browser tests, security, and policy gates.
- Tests cover Codex configuration defaults and bounds, administrator
trace actions, persistent settings, ledger inspection, trace lineage,
and Honeycomb links.
- Existing server trace authorization and retention tests remain the
backend authority.
- Local tests were not run. The requested verification policy uses
GitHub Actions for this series.
- `git diff --check runner/task-workspace-experience...HEAD` passes.
- The delta contains 54 files.

## Risks

- Raw provider traces can contain sensitive provider data.
- Existing server authorization controls access, reveal, download,
retention, and deletion.
- The UI gates trace actions by administrator access and developer mode.
- All new instance settings remain off by default.
- Fresh Paperclip Runner configuration remains Codex-only.
- Direct adapters and legacy task behavior do not change in this PR.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex with GPT-5.6. The work used high-reasoning agent mode,
repository tools, GitHub tools, and parallel code-audit agents.

## 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 or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [ ] 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 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:
Dotta authored and GitHub committed 2026-09-01 03:41:23 -05:00
1 parent 39206c0096
commit 131f5c4065
52 files changed
+4490 -178

No files matched your search

@@ -87,6 +87,8 @@ describe("buildPaperclipRunnerConfig", () => {
expect(config).toMatchObject({
provider: "codex",
codexPermissionMode: "untrusted",
lifecycleMode: "per_turn",
model: "gpt-5.4",
timeoutSec: 0,
graceSec: 15,
@@ -107,43 +109,53 @@ describe("buildPaperclipRunnerConfig", () => {
}
});
it("builds a bounded OpenCode runner profile from schema values", () => {
it("persists bounded Codex permission and warm lifecycle values", () => {
const config = buildPaperclipRunnerConfig(makeValues({
adapterType: "paperclip_runner",
model: "",
codexEngine: "acp",
dangerouslyBypassSandbox: true,
adapterSchemaValues: {
provider: "opencode",
opencodePermissionMode: "ask",
lifecycleMode: "warm",
idleTimeoutMs: 45_000,
},
codexPermissionMode: "untrusted",
paperclipRunnerLifecycleMode: "warm",
paperclipRunnerIdleTimeoutMs: 45_000,
}));
expect(config).toMatchObject({
provider: "opencode",
model: "openrouter/deepseek/deepseek-v4-flash-0731",
opencodePermissionMode: "ask",
provider: "codex",
codexPermissionMode: "untrusted",
lifecycleMode: "warm",
idleTimeoutMs: 45_000,
});
expect(config).not.toHaveProperty("engine");
expect(config).not.toHaveProperty("dangerouslyBypassApprovalsAndSandbox");
});
it("falls back to safe OpenCode defaults for invalid schema values", () => {
it("fails closed to the Codex profile and safe defaults for stale schema values", () => {
expect(buildPaperclipRunnerConfig(makeValues({
adapterSchemaValues: {
provider: "opencode",
opencodePermissionMode: "unrestricted",
codexPermissionMode: "unrestricted",
lifecycleMode: "forever",
idleTimeoutMs: -1,
},
}))).toMatchObject({
provider: "opencode",
opencodePermissionMode: "allow",
provider: "codex",
codexPermissionMode: "untrusted",
lifecycleMode: "per_turn",
});
});
it("bounds warm lifecycle values to the shared safe default", () => {
expect(buildPaperclipRunnerConfig(makeValues({
paperclipRunnerLifecycleMode: "warm",
paperclipRunnerIdleTimeoutMs: 86_400_001,
}))).toMatchObject({
lifecycleMode: "warm",
idleTimeoutMs: 300_000,
});
});
it("omits an idle timeout for turn-by-turn sessions", () => {
const config = buildPaperclipRunnerConfig(makeValues({
paperclipRunnerLifecycleMode: "per_turn",
paperclipRunnerIdleTimeoutMs: 45_000,
}));
expect(config).not.toHaveProperty("idleTimeoutMs");
});
});
@@ -1,4 +1,9 @@
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
import {
buildAdapterEnvConfig,
resolvePaperclipRunnerIdleTimeoutMs,
resolvePaperclipRunnerPermissionMode,
type CreateConfigValues,
} from "@paperclipai/adapter-utils";
import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "../index.js";
function parseCommaArgs(value: string): string[] {
@@ -61,14 +66,6 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unk
return ac;
}
function paperclipRunnerProvider(value: unknown): "codex" | "opencode" {
return value === "opencode" ? "opencode" : "codex";
}
function openCodePermissionMode(value: unknown): "allow" | "ask" | "deny" {
return value === "ask" || value === "deny" ? value : "allow";
}
/** Build a provider profile accepted by the experimental Rust runner. */
export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string, unknown> {
const config = buildCodexLocalConfig(v);
@@ -91,24 +88,19 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
delete config[unsupportedKey];
}
const schemaValues = v.adapterSchemaValues ?? {};
const provider = paperclipRunnerProvider(schemaValues.provider);
if (provider === "codex") {
return { ...config, provider };
}
const lifecycleMode = schemaValues.lifecycleMode === "warm" ? "warm" : "per_turn";
const configuredIdleTimeoutMs = schemaValues.idleTimeoutMs;
const idleTimeoutMs = typeof configuredIdleTimeoutMs === "number"
&& Number.isSafeInteger(configuredIdleTimeoutMs)
&& configuredIdleTimeoutMs > 0
? configuredIdleTimeoutMs
: 300_000;
const lifecycleCandidate = v.paperclipRunnerLifecycleMode ?? schemaValues.lifecycleMode;
const lifecycleMode = lifecycleCandidate === "warm" ? "warm" : "per_turn";
const configuredIdleTimeoutMs =
v.paperclipRunnerIdleTimeoutMs ?? schemaValues.idleTimeoutMs;
const idleTimeoutMs = resolvePaperclipRunnerIdleTimeoutMs(
configuredIdleTimeoutMs,
);
return {
...config,
provider,
model: v.model || "openrouter/deepseek/deepseek-v4-flash-0731",
opencodePermissionMode: openCodePermissionMode(
schemaValues.opencodePermissionMode,
provider: "codex",
codexPermissionMode: resolvePaperclipRunnerPermissionMode(
"codex",
v.codexPermissionMode ?? schemaValues.codexPermissionMode,
),
lifecycleMode,
...(lifecycleMode === "warm" ? { idleTimeoutMs } : {}),