9.4 KiB
Phase 6: Run the Thin Paperclip Adapter
Phase 6 connects one local, issue-bound codex_local task to the runner's
public ControlPlanePort/NativeSessionBackend contract. Selection is
default-off, explicit per agent, and persisted before provider execution. The
runner never receives a Paperclip API key, local JWT, managed MCP token, raw
environment, wake payload, or skill instructions.
Prerequisites
- a local Paperclip instance built from this branch;
- a board/operator API key in
PAPERCLIP_API_KEY(never print it); PAPERCLIP_API_URL,PAPERCLIP_AGENT_ID, andPAPERCLIP_TASK_ID;- an active
codex_localagent, a standard issue assigned to it, and a realized local execution workspace; - Codex already authenticated on the host.
Normalize the API base once:
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
PHASE6_SCRATCH="${PAPERCLIP_RUN_SCRATCH_DIR:-$(mktemp -d)}"
1. Prove the mock package contract
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/backends/harness-driver-backend.test.ts \
src/contracts/native-execution.test.ts \
src/native-session-runtime.test.ts
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
--target mock --scenario happy-path
Expected: six targeted tests pass and the trace reports resolvedMode as
native, three replay-stable events, a successful workspace barrier, and one
mock server-owned done decision. This tracer does not connect to Paperclip.
2. Prove the database-backed Paperclip port
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/native-runner-phase6.integration.test.ts \
src/__tests__/heartbeat-native-runner-selection.test.ts \
src/__tests__/heartbeat-native-runner-cancellation.test.ts \
src/__tests__/heartbeat-run-event-sequencing.test.ts \
src/__tests__/native-runner-input-boundary.test.ts \
src/__tests__/native-run-finalizer.test.ts \
src/__tests__/native-status-arbiter-corpus.test.ts \
src/__tests__/native-interaction-bridge.test.ts \
src/__tests__/native-session-resumption.test.ts \
src/__tests__/native-finalization-recovery.test.ts \
src/__tests__/native-finalization-migration.test.ts \
src/__tests__/legacy-finalization-regression.test.ts \
src/services/native-runtime/native-session-executor.test.ts
This runs the unchanged package conformance suite against the real Paperclip port. Forty-two targeted tests check company binding, duplicate/gap replay, checkpoint recovery, immutable result ingestion, durable-evidence authority, workspace-gated finalization, atomic liveness rollback, status-version CAS, migration repair, legacy byte equivalence, typed interaction materialization, same-run pre-result resumption, lease races, and audit output. All database tests start embedded PostgreSQL and do not skip when a developer database URL is absent.
The corpus cases dispatch fixture-specific database shapes to named production consumers and derive all eleven expected fields from their returns and persisted rows: run status, status/preserve action, reason, required and forbidden effects, live-path kind, claim preservation, native-record behavior, decision count, maximum wake count, and maximum notification count. Every one of the 70 matrix rows requires evidence from its responsible finalizer, terminal projection, attention, cancellation, committer, reconciliation, compatibility, or migration consumer; a missing call or changed return fails the row. Each native effect must also have a delivered, company-bound target row or concrete target-state mutation, and replay must retain one decision and one delivery attempt. A separate unknown-effect case proves the status transaction fails closed without partial rows. The recovery case uses a scripted provider only at the backend boundary while the real heartbeat reaper, lease claim, original run execution, persistence port, finalizer, and terminal projection execute.
The command above is the repeatable scripted internal canary and actual flag-off legacy-heartbeat proof. It is not a live-provider canary. The remaining sections are an optional live-provider rollout procedure; they were not run during remediation and require explicit operator authority to change an instance flag and agent profile.
3. Enable one isolated agent
Save the agent's current runtime profile, merge the native profile, then enable the instance flag. These endpoints require board/operator authority.
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" \
| jq '.runtimeConfig' > "$PHASE6_SCRATCH/phase6-runtime-before.json"
jq -n \
--slurpfile current "$PHASE6_SCRATCH/phase6-runtime-before.json" \
'{runtimeConfig: ($current[0] * {nativeRunner: {mode: "native", backend: "codex_app_server", protocolVersion: 1}})}' \
| curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" >/dev/null
curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enableNativeRunner":true}' \
"$PAPERCLIP_API_BASE/api/instance/settings/experimental" >/dev/null
4. Run one local native Paperclip task
PAPERCLIP_NATIVE_RUN_ID="$({
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"on_demand\",\"triggerDetail\":\"manual\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_ID\"},\"forceFreshSession\":true}" \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID/wakeup"
} | jq -r '.id')"
while :; do
PAPERCLIP_NATIVE_STATUS="$({
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID"
} | jq -r '.status')"
case "$PAPERCLIP_NATIVE_STATUS" in
succeeded|failed|cancelled|timed_out) break ;;
esac
sleep 2
done
test "$PAPERCLIP_NATIVE_STATUS" = succeeded
The expected run has runtimeMode=native, a persisted completion contract and
runner instance, no legacy adapter invocation, and a typed terminal fact.
5. Inspect replay and finalization
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID/events?afterSeq=0&limit=200" \
| jq '[.[] | select(.payload.prpEvent != null)] | {count: length, events: map({seq, sourceSeq, sourceEventId, eventType})}'
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID" \
| jq '{runtimeMode, nativePhase, runnerInstanceId, completionContractId, resultJson}'
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/issues/$PAPERCLIP_TASK_ID" \
| jq '{status, statusVersion, lastStatusDecisionId, unblockDescriptor}'
Repeat the events request with afterSeq=<seq>. Replay is exclusive and stable;
native result/finalization rows remain authoritative even if the flag changes.
6. Exercise cancellation and budget hard-stop
Start a deliberately long native task, then use the existing cancellation API:
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID/cancel" >/dev/null
The same run-scoped native cancel handle is invoked by explicit cancellation, agent pause, and budget-pause enforcement. Resource, environment, scratch, and workspace cleanup remain owned by the existing heartbeat path.
7. Disable the flag and prove legacy fallback
Set PAPERCLIP_LEGACY_TASK_ID to a fresh eligible issue assigned to the same
agent; the native proof issue may already be terminal.
curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enableNativeRunner":false}' \
"$PAPERCLIP_API_BASE/api/instance/settings/experimental" >/dev/null
PAPERCLIP_LEGACY_RUN_ID="$({
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"on_demand\",\"triggerDetail\":\"manual\",\"payload\":{\"issueId\":\"$PAPERCLIP_LEGACY_TASK_ID\"},\"forceFreshSession\":true}" \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID/wakeup"
} | jq -r '.id')"
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_LEGACY_RUN_ID" \
| jq '{runtimeMode, runtimeModeReason}'
Expected: the new run reports legacy and instance_flag_disabled; an already
selected native run still reconciles from its persisted native coordinator and
never falls through to the legacy adapter.
Restore the saved profile after the proof:
jq -n \
--slurpfile saved "$PHASE6_SCRATCH/phase6-runtime-before.json" \
'{runtimeConfig: $saved[0]}' \
| curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" >/dev/null
Stop conditions
Stop if a native failure invokes legacy, credentials appear in model input or stored PRP bytes, a workspace-finalization failure reports success, replay changes bytes, or a model-reported disposition bypasses the server arbiter.