mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - New users often connect a Codex agent with an OpenAI API key during
onboarding.
> - A user asked such an agent for a logo. The agent said that the
built-in image generator was not available and that its fallback needed
a separate API key. It then drew an SVG by hand.
> - #19516 removed `--disable image_generation`. That change is correct,
but it does not reach this path. The pinned Codex 0.160.0 shows its
`image_gen` tool only when Codex signs in with ChatGPT. The check is
`image_generation_available` in `codex-rs/core/src/tools/spec_plan.rs`.
An API key never gets the tool.
> - The runner keeps the provider key out of the agent's shell on
purpose, so the skill's `scripts/image_gen.py` fallback cannot run.
> - This pull request adds a Paperclip `generate_image` tool. The server
makes the image with the agent's existing OpenAI key. The key never goes
to the agent process or the shell.
> - The benefit is that API-key Codex agents can make images with no
second key. The spend goes to the agent's budget and the budget can stop
it.
## Linked Issues or Issue Description
Refs #19516
**What happened?**
An agent connected through onboarding with an OpenAI API key (Codex
harness, `gpt-5.6-sol`) could not generate images. The agent said: "The
image skill's built-in generator isn't available in this run, and its
fallback would require a separate API key." It then drew an SVG instead.
**Steps to reproduce**
1. Create a company with a Codex agent that uses an OpenAI API key.
2. Ask the agent for a raster image, for example a logo PNG.
3. The agent reports that image generation is not available. It does not
make the image.
**Expected behavior**
The agent makes the image with the OpenAI key that already powers it.
**Root cause**
The agent ran on the native runner's Codex app-server path, with
`auth.json = {"OPENAI_API_KEY": ...}`. Codex 0.160.0 removes
`image_gen.imagegen` from the model's tools unless
`current_auth_uses_codex_backend()` is true. That function is true only
for a ChatGPT sign-in.
I proved this with the real pinned binary. I gave it the runner's
isolated launch arguments and the API-key auth file, and pointed it at a
local Responses endpoint. The first model request offers only `exec`,
`wait`, `request_user_input` and the collaboration tools. #19516's test
checked only `codex features list`, so it could not see this gate.
## What Changed
- **New `server/src/services/native-runtime/generate-image-tool.ts`:**
- The tool schema and definition.
- `imageGenerationBindingForRun`. It applies only to native Codex runs
on a direct OpenAI `api_key` connection with no router selection.
- The OpenAI Images API call (`gpt-image-2`, `b64_json`, PNG check, 180
s timeout). The reply is read as a stream with a byte cap before it is
parsed. A decoded image larger than 20 MiB is refused.
- A reviewed rate card for `gpt-image-2`: text input $5, image input $8
and image output $30 per 1M tokens, from
developers.openai.com/api/docs/pricing, retrieved 2026-11-06.
- A create-only, symlink-confined local write.
- **`paperclip-runner-tool-authority.ts`:**
- Adds the `imageGeneration` binding.
- Advertises the tool only for standard mode with a workspace. A remote
target must also have an installer. Dot, Muse, Grok and native review do
not get the tool.
- Adds the `#generateImage` handler. Before any spend, it checks again:
the run and task, budget authority, `issue:mutate`, case handoff, and
the connection.
- Stores a claim-before-spend per-call receipt in
`resultJson.imageToolReceipts`. A retry or recovery replays the receipt
and does not pay again.
- Saves a `charged` receipt (usage, price, request ID and charge time)
as soon as OpenAI returns. A failed save, failed accounting write or
crash can then still settle the spend. A retry or recovery records the
cost once and never buys again.
- Records one `cost_events` row through `createCostEventInTransaction`.
The row is `metered_api` with `costStatus: "estimated"`, and the budget
is evaluated. The row is recorded even if saving fails.
- Checks the task's project budget, as well as the company and agent
budgets, before spending.
- Writes content-free `runner.image_generate` activity entries: started,
completed, failed and unconfirmed.
- **`ai-connections.ts`:** adds `selectImageGeneration`. It checks again
that the connection is an active, healthy OpenAI `api_key` connection
with no custom routing. It also checks the responsible user's
membership, the grant audience and the agent or company installation.
These are the same rules that runtime selection uses.
- **`native-session-executor.ts`:** gets the binding from the run's
admitted `contextSnapshot.aiConnection`. The value comes only from the
server, never from tool input.
- **`native-session-resume.ts`:** the native tool contract changes from
`v18` to `v19` and now lists `generate_image`. A persisted provider
thread cannot add tools when it resumes, so older threads must rotate.
- **`doc/runner-api-tools.md`:** a new section about image generation
for OpenAI API-key Codex agents.
- **#19516: kept, not reverted.** Its changes have no effect on the
API-key path. They are still correct for Codex agents that sign in with
ChatGPT, because `image_gen` works there and saves to
`CODEX_HOME/generated_images`. Those agents need the read grant that
#19516 added. On the API-key path, the new tool replaces the built-in
tool. The tool's description tells the agent to use it instead of
`image_gen` or `scripts/image_gen.py`.
## Verification
**Red on `master` (e482f0f632).** Behavioural red with the first version
of the new test:
```
FAIL src/services/native-runtime/generate-image-tool.test.ts > generate_image for OpenAI API-key Codex agents (N-12) > lets an API-key Codex agent make an image with its existing OpenAI key, saved in its workspace and billed
AssertionError: expected [ 'connections_search', …(51) ] to include 'generate_image'
❯ src/services/native-runtime/generate-image-tool.test.ts:78:60
Tests 1 failed | 3 passed (4)
```
The final test file on master's source also fails. The reason is that
the module does not exist: `Cannot find module
'./generate-image-tool.js'`.
**Real pinned Codex binary.** The new test is
`packages/paperclip-runner/src/drivers/codex/codex-api-key-image-tools.test.ts`.
It uses the runner's isolated launch, the API-key `auth.json` and a
local Responses endpoint. It shows that the model gets no `image_gen`,
`imagegen` or `image_generation` tool, and that a server-advertised
`generate_image` tool does reach the model:
```
Test Files 1 passed (1)
Tests 1 passed (1)
```
**Green after the fix:**
`server/src/services/native-runtime/generate-image-tool.test.ts` uses
embedded PostgreSQL and a stubbed OpenAI fetch.
```
Test Files 1 passed (1)
Tests 11 passed (11)
```
The 11 tests cover these behaviours:
- **Main path.** The tool is advertised. The request goes to
`api.openai.com/v1/images/generations` with the existing key. The PNG is
written to the workspace. The key does not appear in the result or in
activity. One `cost_events` row is written: 50 text tokens and 1000
output tokens, which is $0.03025, estimated, from the rate card. A retry
and `recover()` replay the receipt with no second fetch and no second
cost row.
- **No binding:** the tool is not offered and `execute` refuses.
- **Budget hard-stop.** The stop pauses the agent, so the call is
refused before any fetch and no file is written.
- **Bad grant:** the call is refused before any fetch.
- **Project budget stop.** The task's project is paused by its budget,
so the call is refused before any fetch.
- **Charge settlement.** A crash left a `charged` receipt. The retry and
`recover()` record the cost once, report the image path and do not buy
another image.
- **Oversized reply.** The read stops before the body is parsed.
- **Save failure.** The spend is still recorded. A same-call retry does
not pay again and reports the failure.
- **Binding gate.** Only Codex on a direct OpenAI API key gets the
binding. A subscription, ACPX, OpenRouter or a routed connection does
not.
- **Rate card math.**
- **Provider errors.** The error text does not contain the key, and
non-PNG data is rejected.
**Neighbouring suites:**
- `src/drivers/codex` in the runner package: 24 files, 350 tests passed.
- Server: `paperclip-runner-tool-authority.test.ts`,
`native-session-resume.test.ts`, `fast-responses.test.ts`,
`ai-connection-session.test.ts` and the new suite. 157 of 158 passed.
- The one failure is `native-session-resume.test.ts › archives a damaged
prior epoch … with real runnerd` (ENOENT on `authority-epochs`). It
fails the same way on unmodified `master` in this local environment, so
this change did not cause it.
- `tsc --noEmit -p server/tsconfig.json`: clean.
- `pnpm verify` exits 2 (broad). Required CI supplies the broad
coverage.
## Risks
- **New security surface: this needs maintainer sign-off.** The server
now sends the agent's OpenAI API key to `api.openai.com` on the agent's
request. The tool is limited in these ways:
- The key stays in the server.
- Each call checks run, task, budget, mutation and connection authority
again.
- The destination name comes from the server and must match
`^[a-z0-9-]+-[0-9a-f]{8}\.png$`. The write is create-only and confined
to the workspace root. On macOS it uses `O_NOFOLLOW_ANY`. On Linux it
uses a `/proc` descriptor walk.
- **Spend.** Each call costs money. The tool description tells the agent
to make only what the task needs. The cost is an estimate from a rate
card (`costStatus: "estimated"`), not an invoice. The rate card applies
the image-output rate to all output tokens, so the estimate errs on the
high side.
- **Checkpoint rotation.** The tool-contract bump from v18 to v19 makes
existing native provider threads start fresh once at their next turn.
This is what the contract requires when the tool surface changes.
- **Pricing drift.** If OpenAI changes `gpt-image-2` prices, the rate
card must be updated.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, sub-agents) and extended
reasoning.
## 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
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
363 lines
21 KiB
Markdown
363 lines
21 KiB
Markdown
# Runner API escape hatch
|
||
|
||
`search_api` and `call_api` extend the native runner when an available dedicated
|
||
operation cannot express the requested work. Use `set_task_monitor` to schedule or clear a one-shot task check; after confirming
|
||
the receipt, finish with `yielded` and `continuation.kind: "monitor"` to await that
|
||
task’s timer. The monitor workflow remains preferred; REST calls use the same route authorization as legacy agents.
|
||
Existing tools remain preferred;
|
||
agents do not have to search before using them. Only two tool definitions are
|
||
advertised. The API catalog is returned on demand, never injected into the
|
||
initial prompt.
|
||
|
||
## Default availability and operator controls
|
||
|
||
The escape hatch is enabled by default. No environment variable is required.
|
||
The native `hire_agent` tool shares this availability policy.
|
||
Set `PAPERCLIP_RUNNER_API_TOOLS_ENABLED=false` on the server to disable these
|
||
tools. An explicit `true` also enables them; other explicit values fail closed.
|
||
|
||
Operators can restrict availability with `PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS`,
|
||
a comma-separated list of company UUIDs. An unset list allows every company;
|
||
an explicitly empty list allows none. IDs must match exactly. This restriction
|
||
also applies when the enabled flag is unset.
|
||
|
||
A server-owned binding can disable these tools for a baseline eval but cannot
|
||
override an operator restriction. The server checks the policy when advertising
|
||
tools, when accepting a call, and immediately before HTTP dispatch after
|
||
preparing any files. Existing dedicated tools remain available. Operators must
|
||
update the environment of each server process and restart it for deployment-level
|
||
changes; this environment switch is not a live settings API.
|
||
|
||
All company, run, work-mode, credential, and route authorization checks below still apply.
|
||
|
||
## Discovery and requests
|
||
|
||
```json
|
||
{"query":"create project","limit":5}
|
||
```
|
||
|
||
Search is deterministic lexical ranking over OpenAPI paths, summaries and the
|
||
old skill reference. It supports task/issue and other terminology, exact
|
||
`METHOD /api/path/{parameter}` lookup, and opaque query/catalog-bound pagination.
|
||
Results include resolved request schemas, response descriptions, authorization
|
||
metadata, work modes, examples where available, and relevant dedicated tools
|
||
with their supported parameters. `limit` defaults to five and is capped at ten.
|
||
|
||
```json
|
||
{"operationId":"PATCH /api/projects/{id}","pathParams":{"id":"PROJECT_UUID"},"body":{"description":"Updated project description"}}
|
||
```
|
||
|
||
The catalog determines method and path. `companyId` is filled from the active
|
||
binding. Scalars and arrays are accepted in `query`. `body` defaults to JSON;
|
||
`contentType` supports text and raw uploads. `files` accepts entries containing
|
||
exactly one authorized `artifactId` or task-workspace `path`, and an optional
|
||
multipart `field`. No arbitrary URL, headers, authentication, or remote file URL
|
||
can be supplied. Routes still validate payloads and enforce permissions.
|
||
|
||
Requests have a 16 KiB URL limit and 10 MiB request/upload limit. **New response
|
||
captures are limited to 1 GiB of decoded bytes.** This is separate from the 24 KiB
|
||
inline/page limit. The receiver rejects an oversized Content-Length before
|
||
reading and counts actual bytes before writing, including chunked or compressed
|
||
responses. Oversized responses return `api_response_too_large`; narrow the query
|
||
or use the endpoint's own pagination. A mutation may already have committed, so
|
||
inspect its state rather than retrying it to obtain a smaller response.
|
||
|
||
Connection setup and stalled response reads time out after 30 seconds. An active
|
||
capture has a 10-minute total download deadline. Responses above 24 KiB stream
|
||
into a private temporary file, then into a company-owned asset scoped to the
|
||
issuing agent and responsible user (plus authorized board administrators).
|
||
Ordinary task or run visibility does not grant access to another actor’s API
|
||
snapshot. Explicit deliverables use the existing attachment/work-product flow. Memory stays
|
||
bounded by the inline prefix and stream buffers. Binary responses also become
|
||
assets; text previews are limited to 2,000 bytes.
|
||
|
||
Large captures reserve 1 GiB against a **durable 4 GiB per-run capture budget**
|
||
before creating a file. A completed capture settles to its actual byte count;
|
||
failed/interrupted captures retain the full reservation to bound retry loops.
|
||
A process restart does not reset that budget. Small inline responses and reads
|
||
of existing assets need no reservation. Concurrent large captures are limited
|
||
to **two per company and four per server process**, including the storage upload
|
||
and temporary-file cleanup. Budget exhaustion returns `api_response_capture_limit`;
|
||
concurrency exhaustion returns `api_response_capture_busy` without queuing more
|
||
large transfers. A **20 GiB company-wide quota** counts all stored `runner-api` snapshots plus
|
||
unattached reservations. Admission uses a company database lock, so runs and
|
||
server processes share the same quota. Existing snapshots from before this
|
||
change count too. Attaching an asset converts its reservation to actual stored
|
||
bytes; deleting the asset frees that capacity. Small binary snapshots also need
|
||
storage admission. Ordinary inline text/JSON and existing-asset pages do not.
|
||
|
||
Operators can set `PAPERCLIP_RUNNER_API_COMPANY_CAPTURE_MAX_BYTES` to a positive
|
||
safe integer of at least 1 GiB. Invalid values fall back to 20 GiB. No zero or
|
||
unlimited setting is accepted. This quota covers API snapshots, not all company
|
||
attachments. Storage capacity and backend limits still apply.
|
||
|
||
Handled pre-storage failures release the company reservation after temporary
|
||
file cleanup, but retain the run's charge against retry loops. After a metadata
|
||
transaction fails, a locking read must prove the asset was not committed before
|
||
the uploaded object is removed. Confirmed cleanup refunds the company quota. A crash, failed
|
||
cleanup, or ambiguous storage write keeps an unattached reservation. Operators
|
||
must reconcile possible orphan files/objects before deleting that reservation
|
||
from `runner_api_response_reservations`; no automatic expiry silently refunds
|
||
possibly occupied space. Linked rows are removed with their asset. Deleting a
|
||
run does not release its unattached company reservations.
|
||
|
||
Temporary files are removed on success or failure. Long captures revalidate the
|
||
active run at least every MiB or at the next chunk after one second, and again
|
||
before returning the snapshot. Stopping the run stops its download.
|
||
|
||
To inspect saved text without creating another artifact, call its authorized
|
||
content operation with `responseText`:
|
||
|
||
```json
|
||
{"operationId":"GET /api/assets/{assetId}/content","pathParams":{"assetId":"RETURNED_ARTIFACT_ID"},"responseText":{"offsetBytes":0,"limitBytes":8192}}
|
||
```
|
||
|
||
The result contains `data` as text (including JSON), plus `responseText` with
|
||
`offsetBytes`, `nextOffsetBytes`, and `totalBytes`. Continue at `nextOffsetBytes`
|
||
until it is null. Windows end at UTF-8 boundaries. The limit defaults to 24 KiB
|
||
and accepts 4–24,576 bytes. The server rejects binary content, invalid UTF-8,
|
||
and offsets inside a code point or beyond the response. This option only works
|
||
with GET; never repeat a mutation to retrieve another part of its response.
|
||
Read the saved artifact for a stable snapshot instead of paging a changing live
|
||
response. A text-window call against a live response above 24 KiB also returns
|
||
that snapshot's artifact reference; continue on its content operation. A saved
|
||
asset page never creates another asset. An unpaged asset read also fetches only
|
||
a bounded preview and returns the existing reference instead of copying the file. Offsets and total sizes use safe integer
|
||
byte counts, including values above 2 GiB. Existing assets larger than the 1 GiB
|
||
capture limit remain readable because each request transfers only a bounded range.
|
||
Request/upload limits and all route
|
||
authorization remain.
|
||
Saved asset pages use authenticated HTTP byte ranges. The storage provider reads
|
||
only the requested window, with at most two extra bytes for UTF-8/EOF handling.
|
||
The client validates `Content-Range`, the total size, and the received byte count;
|
||
it rejects unsupported or inconsistent ranges instead of downloading the whole
|
||
asset for every page. Other GET routes are fetched once in full to create the
|
||
snapshot, so use the returned asset for subsequent pages. S3 uses streamed
|
||
multipart uploads for large snapshots. Asset sizes are stored as PostgreSQL
|
||
`bigint`, preserving the existing numeric API shape.
|
||
|
||
### Verified artifact installation
|
||
|
||
Native standard-mode runs can use `download_artifact` to install an authorized
|
||
saved API artifact into the admitted execution workspace. Pass `artifactId`, a
|
||
workspace-relative `destination`, and the exact saved `byteSize` and `sha256`.
|
||
The parent directory must already exist. The transfer is bounded to 10 MiB;
|
||
larger snapshots remain readable through the existing paged content API.
|
||
|
||
The controller reuses asset content authorization (including company, capture
|
||
owner and responsible-user restrictions), checks the complete stored bytes, then
|
||
uses the lease-owned command runner for remote execution. The execution host
|
||
checks size and SHA-256 on a private staging file before atomic create-only
|
||
publication. Linux directory descriptors reject parent/destination symlinks and
|
||
traversal. Existing different files are never overwritten; an identical retry
|
||
verifies the existing file. A lost promotion acknowledgement is an unknown outcome:
|
||
the native recovery path rechecks current authority and verifies the destination
|
||
against the same size and hash without installing again. Missing or conflicting
|
||
destinations return tool errors. An explicit identical retry remains safe. Interrupted staging can
|
||
leave a reserved scratch file if cleanup cannot reach the provider, but never a
|
||
completed destination. File synchronization after the turn remains owned by the
|
||
normal workspace lifecycle.
|
||
|
||
Current support is Linux local execution and remote native command-runner
|
||
transports (including Daytona and SSH). Plugin-owned tasks without an installer,
|
||
Dot, Muse, Grok and nonstandard work modes do not advertise this capability.
|
||
The API-tool rollout policy can disable it. Every attempt rechecks live task/run
|
||
and content authority before storage reads and promotion, and writes content-free
|
||
`runner.artifact_install` audit records. No API credentials enter the execution
|
||
command. Stored content remains untrusted: installation does not promote source
|
||
trust, execute artifact instructions, or provide an alternative for content that
|
||
screening denied. Existing API paging, upload and deliverable tools keep their
|
||
current contracts.
|
||
|
||
The synthetic regression in
|
||
`server/src/services/native-runtime/artifact-transfer.integration.test.ts` verifies
|
||
388131 Unicode/escape-containing bytes and independent downstream hashing with
|
||
real PostgreSQL and filesystem operations. Filesystem fault tests exercise the
|
||
actual command with simulated provider transport; they do not establish a live
|
||
Daytona RPC or deployed recovery.
|
||
|
||
|
||
### Image generation for OpenAI API-key Codex agents
|
||
|
||
Codex shows its built-in `image_gen` tool only when it signs in with ChatGPT.
|
||
A Codex agent that uses an OpenAI API key does not get that tool. The runner
|
||
also keeps the provider key out of the agent's shell, so the imagegen skill's
|
||
`scripts/image_gen.py` fallback cannot run.
|
||
|
||
For these agents, the controller advertises `generate_image`. The tool is
|
||
advertised only when all of these conditions are true:
|
||
|
||
- The run is a native Codex standard-mode run.
|
||
- The run was admitted on an OpenAI `api_key` connection with no custom routing.
|
||
- The run has a workspace.
|
||
|
||
Dot, Muse, Grok, native reviews and plugin-owned tasks do not get the tool.
|
||
|
||
The server reads the key from that same connection on each call. It sends the
|
||
key only to `https://api.openai.com/v1/images/generations` (model
|
||
`gpt-image-2`). The key never enters the provider process, the execution
|
||
command or the tool result.
|
||
|
||
Every call does these checks again before any spend:
|
||
|
||
- The live run, task and budget authority.
|
||
- `issue:mutate` permission.
|
||
- The connection grant, audience and agent installation.
|
||
|
||
A budget hard-stop that pauses the agent refuses the call before OpenAI is
|
||
contacted.
|
||
|
||
The PNG is saved as a new, create-only file in the workspace root:
|
||
|
||
- Local hosts use symlink-confined opens.
|
||
- Remote targets use the same installer as `download_artifact`.
|
||
|
||
The result returns the relative path, the size and the SHA-256.
|
||
|
||
Each generated image records one `cost_events` row (`metered_api`,
|
||
`costStatus: "estimated"`) from a reviewed `gpt-image-2` rate card. The row is
|
||
recorded even when saving the image fails. Each call also writes
|
||
content-free `runner.image_generate` audit records.
|
||
|
||
A per-call receipt in the run result makes a retried or recovered call replay
|
||
the first result instead of paying again.
|
||
|
||
### Media files and upload limits
|
||
|
||
The 1 GiB limit covers new snapshots returned by `call_api`, such as large JSON
|
||
exports or binary API downloads. It does not raise attachment upload limits or
|
||
limit files an agent creates and edits inside its workspace. Saved asset downloads
|
||
stream from storage and support byte ranges, including video seeking.
|
||
|
||
`PAPERCLIP_ATTACHMENT_MAX_BYTES` separately defaults to 10 MiB for uploads and
|
||
native file handoffs. `call_api` uploads also have their own 10 MiB limit. Several
|
||
upload and handoff paths buffer complete files in memory; raising those defaults
|
||
to GiB sizes requires streaming ingestion and corresponding admission/budget
|
||
controls first. For a future video attachment workflow, 2 GiB per streamed file
|
||
is a reasonable default, with an operator override and storage quotas. Do not
|
||
claim that this response-paging change enables GiB attachment uploads.
|
||
|
||
Tool responses identify the HTTP route with `apiOperationId`. The native protocol
|
||
reserves `operationId` and `callId` for semantic tool-call identity; API metadata
|
||
must not masquerade as that envelope. Saved mutation receipts are normalized at
|
||
the tool boundary as well, without repeating their HTTP request.
|
||
All redirects are refused. Interrupted mutation responses have an
|
||
unknown outcome, requiring inspection before another mutation.
|
||
Mutation responses with HTTP 5xx, HTTP 408, redirects, or malformed JSON also
|
||
retain an unknown outcome. A server may have committed the write before it
|
||
failed to return a valid response.
|
||
|
||
## Authority and replay
|
||
|
||
The server revalidates the active native run, assigned task and actor, then
|
||
creates a server-held agent JWT bound to that company and run. Requests go
|
||
through the actual HTTP router with its authorization, validation and domain
|
||
audit behavior. Skill-test runs retain the same task-scoped `skill_test` JWT
|
||
as legacy REST runs, including after later edits to the task mode. An additional `runner.api_called` receipt attributes mutations
|
||
to the run even where older route audit events omit that field.
|
||
The run and work mode are checked again after asynchronous file preparation, so
|
||
a stopped run cannot dispatch an upload prepared under its earlier binding.
|
||
|
||
`call_api` is an authenticated REST transport, with the same endpoint and payload
|
||
permissions as a legacy agent using REST. It does not maintain a second denylist
|
||
for routines, runtime services, skill tests, recovery, invitations, secrets,
|
||
exports, lifecycle fields or approval routes. Existing HTTP routes enforce their
|
||
company/resource scope, ownership, grants, redaction, run requirements and board
|
||
or approval gates. A board-only route still denies an agent call. Dedicated tools
|
||
remain preferred for common workflows, including native completion and monitors.
|
||
|
||
Ask and pre-acceptance Plan retain their existing read restrictions and specific
|
||
mutation exceptions (task titles and conversational confirmation recording).
|
||
Protocol endpoints still require their existing clients. Active-run validation,
|
||
server-owned credentials, file containment, transfer bounds and replay handling
|
||
also remain in place. REST parity does not grant board identity or new permissions.
|
||
|
||
Mutation call IDs reserve a durable receipt in the run's existing `resultJson`
|
||
before dispatch. Replays return the recorded result. Reusing an ID with different
|
||
arguments is rejected. A crash after reservation leaves an unknown outcome and
|
||
never automatically resends the mutation. The limit is 512 mutation receipts per
|
||
run. No database migration is needed.
|
||
|
||
Workspace uploads use the existing workspace resource containment checks,
|
||
no-symlink file opens covering every path component, and bounded descriptor reads.
|
||
Local uploads require Linux or macOS; authorized artifacts work on other hosts.
|
||
Artifacts must belong to the bound company and pass the same actor access checks
|
||
as downloads, before and after reading their bytes. Private API response snapshots
|
||
cannot be used as public company logos or bundled into company exports. An owner
|
||
can explicitly publish a copy through the existing attachment upload path.
|
||
Request bodies and uploads go
|
||
through the route's validation and authorization; the fallback does not reject
|
||
fields that an agent can supply through REST. Route-authorized responses normally use the bounded inline/artifact capture and
|
||
replay mechanism. Credential-bearing responses (secret values, issued keys,
|
||
invites, webhook secrets (including restored triggers) and gateway credentials) stay inline, with a 10 MiB
|
||
response bound. They never spill into plaintext files or saved response assets.
|
||
Their durable mutation receipts encrypt the response with the existing local
|
||
secret provider. Replays decrypt it only inside the authorized run; no HTTP
|
||
request repeats. A working secret store is checked before credential dispatch.
|
||
Other API responses retain the existing 24 KiB inline and 1 GiB capture limits.
|
||
|
||
API request and response bodies are omitted from persisted native tool events,
|
||
checkpoints and completion results. Saved events retain operation identity, HTTP
|
||
status and a payload digest for replay conflict detection. The model still receives
|
||
the original authorized HTTP response; this redaction affects durable history.
|
||
Completion retries compare expected and persisted events through the same
|
||
idempotent redaction projection. Repeated projection preserves the original
|
||
payload digest. Provider recovery uses the same projection to compare saved
|
||
completion results with provider history. Changed private bodies still conflict.
|
||
Restart preparation preserves the run's admitted REST credential scope, even
|
||
when the task mode changes. See [the run-log guide](run-log-events.md) for stored fields.
|
||
|
||
The server-issued REST credential for the bound run is never returned to the model.
|
||
Streaming, WebSocket, MCP and authentication
|
||
handshakes are documented as protocol operations requiring their existing clients.
|
||
|
||
## Catalog maintenance
|
||
|
||
`runner-api-catalog.ts` builds from the server OpenAPI registry. Experimental
|
||
pipeline, Cases and smoke-lab routes now share their validators with discovery.
|
||
Seven Cases/pipeline route shapes are multiplexed by resource identity: the Cases
|
||
router intentionally forwards unknown resources to the pipeline router. Their
|
||
separate catalog entries explain which resource identifier is required. Registry
|
||
authorization descriptions are documentation; actual route checks are authoritative.
|
||
|
||
Regenerate old-skill enrichment after editing its API reference:
|
||
|
||
```sh
|
||
node scripts/generate-runner-api-reference.mjs
|
||
node scripts/generate-runner-api-reference.mjs --check
|
||
node scripts/generate-runner-experimental-api-metadata.mjs
|
||
node scripts/generate-runner-experimental-api-metadata.mjs --check
|
||
```
|
||
|
||
Mounted-route coverage tests include experimental routes. Three WebSocket mounts
|
||
are explicitly classified in the catalog. Shared protocol-action catalogs,
|
||
provider projections and generated compatibility checks include both tools.
|
||
|
||
## Verification and paid evals
|
||
|
||
The companion `paperclip-evals` worktree contains `evals/runner-api-tools`.
|
||
Its README documents explicit case/model selectors, the cumulative budget ledger,
|
||
fixture reset, progressive batches, and Evalbook generation. No command defaults
|
||
to running the entire paid suite. Capability, forced operation contracts and
|
||
paired common-operation regressions are reported separately.
|
||
|
||
Provider-free integration tests exercise real runnerd → PRP → authority → HTTP,
|
||
route validation and audit, stale bindings, Ask/Plan restrictions, identity
|
||
spoofing, file containment, uncertain mutation receipts and fixture isolation.
|
||
|
||
The Evalbook viewer uses the existing shared viewer and stylesheet on master.
|
||
The report retains actual persisted-state summaries for private local inspection;
|
||
public replay continues to withhold company-state details.
|
||
|
||
The ACPX sidecar includes the upstream terminal-usage accounting correction from
|
||
`origin/codex/evalbook-default-chat-sept6`. Its qualified Claude executable requires
|
||
Linux x64. The first macOS stage records a zero-cost ACPX admission failure. A later user-authorized
|
||
OpenCode/OpenRouter Sonnet profile reached a real HTTP read, but the attempt failed
|
||
on a missing harness completion contract and incomplete terminal accounting. The
|
||
harness contract is corrected. The missing fourth request was subsequently
|
||
recovered from the matching OpenRouter session and generation billing record;
|
||
the original failed attempt remains immutable. New attempts retain an append-only,
|
||
flushed event journal and bounded provider trace outside disposable runtime files.
|
||
Provider-free startup succeeds for OpenRouter Sonnet and DeepSeek. See
|
||
`doc/plans/2026-09-07-runner-api-production-readiness.md` for remaining release gates.
|