Files
Nicky LeachandPaperclip 38501b1170 Add turn-scoped attachment downloads to native Runner (#20956)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native agents open input images with a file reader.
> - Plugin Runner tasks use an authenticated Runner connection.
> - Runner needs a way to fetch an authorized file from the tenant app.
> - The tenant grants access to the exact attachment for the active run.
> - Runner downloads and verifies it before it returns a local path.

## Linked Issues or Issue Description

Refs #19740. Builds on the attachment authorization and command
transport added in #20939.

## What Changed

- Add the PRP v3 `runner.attachment.download` command for native
workspace providers.
- Issue short-lived grants for exact authorized wake and historical
attachments. Recheck the active run, source, size, and SHA-256 at the
tenant endpoint.
- Download current-wake inputs before provider dispatch. Download
historical files when the authorized chat reader asks for them. Preserve
SSH and sandbox command staging as a fallback.
- Run HTTPS transfers off the Runner control loop. Report completion
with one PRP event per transfer. Drop pending results on cancellation
and clear files at turn settlement, authority rotation, and restart.
- Grant restricted Codex sessions read-only access to the attachment
directory.

## Verification

- 69 targeted server attachment tests passed. They cover wake and
historical downloads, authorization failures, and command fallback.
- 22 focused historical attachment tests passed, including grant
redemption under a held database lock.
- Six focused Rust attachment tests passed, including a source-pinned
Codex sandbox check. The sandbox could read the staged file but could
not modify it or read adjacent Runner state.
- The real Runner transport test confirmed one durable command and one
completion event for a transfer. Restart-continuation tests confirmed
fresh attachment paths appear before dispatch.
- Server and Runner TypeScript checks, Rust formatting, and PRP
generated schema checks passed.
- Required GitHub CI passed on `7a33752bd7`. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/43122967837)
- Greptile scored the same revision 5/5 with no unresolved review
threads.
[Review](https://github.com/paperclipai/paperclip/pull/20956#issuecomment-6808582134)

## Risks

- Saved provider and Runner identities keep their existing formats. A
previous Runner falls back to command staging where the target supports
it. A restart continuation refreshes temporary attachment paths before
dispatch.
- Plugin Runner tasks require a compatible Runner ROM and outbound HTTPS
access to the configured tenant origin.
- Live agent-computer qualification remains a release check.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, shell tools, and
local test execution. Exact variant and context window are unavailable
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: #123` /
`Closes #123` / `Refs #123` or (b) described the issue in-PR following
the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change
- [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-11-11 16:28:02 -08:00

525 lines
12 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v3/command.schema.json",
"title": "PRP v3 runner command",
"type": "object",
"required": [
"schema",
"commandId",
"controllerSeq",
"type",
"issuedAt",
"payload"
],
"properties": {
"schema": {
"const": "paperclip.prp.command.v3"
},
"commandId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"controllerSeq": {
"type": "integer",
"minimum": 1
},
"type": {
"enum": [
"run.prepare",
"run.attach",
"session.open",
"turn.start",
"turn.steer",
"turn.interrupt",
"turn.stop",
"request.resolve",
"interaction.receipt",
"semantic_tool.result",
"session.snapshot",
"session.close",
"session.budget.increase",
"session.destroy",
"run.cancel",
"runner.drain",
"runner.suspend",
"runner.shutdown",
"session.goal.get",
"session.goal.set",
"session.goal.clear",
"external_provider.operation",
"runner.attachment.download"
]
},
"issuedAt": {
"type": "string",
"format": "date-time"
},
"deadlineAt": {
"type": "string",
"format": "date-time"
},
"precondition": {
"type": "object",
"properties": {
"runnerState": {
"type": "array",
"items": {
"type": "string"
}
},
"runState": {
"type": "array",
"items": {
"type": "string"
}
},
"sessionState": {
"type": "array",
"items": {
"type": "string"
}
},
"activeTurnId": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": true
},
"payload": {
"type": "object",
"additionalProperties": true
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "session.goal.set"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"objective": {
"type": "string",
"minLength": 1,
"maxLength": 4000
},
"status": {
"enum": [
"active",
"paused"
]
},
"tokenBudget": {
"type": [
"integer",
"null"
],
"minimum": 1
}
},
"additionalProperties": true
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"type": "object",
"required": [
"requestId",
"bindingId",
"runId",
"normalizedSessionId",
"turnId",
"bindingGeneration",
"assignmentRevision",
"digest",
"action",
"input"
],
"properties": {
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"bindingId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"runId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"normalizedSessionId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"turnId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"bindingGeneration": {
"type": "integer",
"minimum": 1
},
"assignmentRevision": {
"type": "integer",
"minimum": 1
},
"digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"action": {
"enum": [
"accept",
"tool",
"progress",
"finish",
"renew",
"request_user_input",
"consume_input"
]
},
"input": {
"type": "object"
}
},
"additionalProperties": false
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "accept"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "tool"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 160
},
"arguments": {
"type": "object"
}
},
"required": [
"name",
"arguments"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "progress"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"text": {
"type": "string",
"minLength": 1,
"maxLength": 12000
}
},
"required": [
"text"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "finish"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"result": {
"type": "object"
}
},
"required": [
"result"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "renew"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"expiresAtUnixMs": {
"type": "integer",
"minimum": 1
}
},
"required": [
"expiresAtUnixMs"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "request_user_input"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"questionSet": {
"$ref": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json"
}
},
"required": [
"requestId",
"questionSet"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
},
{
"if": {
"properties": {
"type": {
"const": "external_provider.operation"
},
"payload": {
"properties": {
"action": {
"const": "consume_input"
}
},
"type": "object"
}
},
"type": "object"
},
"then": {
"properties": {
"payload": {
"properties": {
"input": {
"type": "object",
"properties": {
"requestId": {
"type": "string",
"minLength": 1,
"maxLength": 240
},
"inputDigest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
}
},
"required": [
"requestId",
"inputDigest"
],
"additionalProperties": false
}
},
"type": "object"
}
},
"type": "object"
}
}
],
"additionalProperties": true
}