Files
PaperClipAI/packages/paperclip-runner/protocol/schemas/question-set.schema.json
T
Dotta 3aa2065d08 feat(runner): validate structured question responses (#12420)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Rust runner now owns a bounded ACPX session and a fail-closed
turn lifecycle
> - Provider questions pause a turn and must return structured answers
to the same persisted question set
> - JSON Schema validates the wire shape, but it cannot validate
identifiers and constraints across two documents
> - Unknown questions, invalid choices, and malformed custom answers
must fail before any provider receives them
> - This pull request adds only the package-local response validator and
tests
> - The benefit is a small trust boundary that later request-resolution
code can use without changing production selection

## Linked Issues or Issue Description

Refs #12419

## What Changed

- Validate `paperclip.question_response.v1` against its versioned JSON
Schema.
- Bound serialized responses to 768 KiB before validation.
- Require answer identifiers to match the exact persisted question set.
- Require answers for required questions and reject unknown question
identifiers.
- Enforce text, single-select, and multi-select answer modes.
- Match the existing TypeScript numeric syntax, including decimal,
exponent, hexadecimal, octal, and binary input.
- Match ECMAScript trimming exactly, including BOM whitespace while
rejecting Unicode NEL rather than inheriting Rust-specific whitespace
behavior.
- Enforce known options, custom-answer policy, text length, pattern, and
numeric constraints.
- Validate duplicate option IDs, inverted bounds, and dynamic patterns
before answer lookup so malformed optional questions fail closed even
when unanswered.
- Match JavaScript UTF-16 code-unit length semantics for text
constraints and the 100,000-unit response-field bound.
- Preserve the public optional `recommended` question-option field in
the versioned schema, generated schema bundle, and Rust validation path.
- Return typed validation errors for malformed inputs without panics.
- Export the validator from the Rust runner core.
- Add table-driven tests for valid, mismatched, malformed, oversized,
and numeric-boundary responses.
- Document the package-local structured-response boundary.
- Add `num-bigint` 0.4 and `num-traits` 0.2 as direct runner-core
dependencies for exact arbitrary-length radix parsing and one-step
JavaScript Number rounding; update only the package-local runner Cargo
lockfile.
- Do not change the repository PNPM lockfile, workflows, runnerd
selection, server behavior, UI, or migrations.

## Verification

- Replay base: `9a9fdf06ee4142f77427db30efccc4c43056f64b` (`master`
after #12419 merged).
- Exact replay head: `fad92b3fb348b66ddb10dde44b7b060e55c4fe96`.
- Stable patch ID: `4d6ffbd519dd081f7ea530977cd965bd4569fc75`; this is
the prepared two-commit delta plus the focused cross-language parity fix
found during replay review.
- The exact delta is 10 files, 712 additions, and 2 deletions, all in
`packages/paperclip-runner`.
- The package-local `packages/paperclip-runner/runner/Cargo.lock`
records the two direct runner-core dependencies; their already-resolved
versions and checksums are unchanged.
- The question-set schema source, generated TypeScript schema bundle,
and protocol manifest hash are updated together; the schema SHA-256 is
`42b5441a3d388851dacb6e4500dfd4a17d878eded2e724228078b647e7440d3f`.
- GitHub Actions run `33366812025`, attempt 2: **PASSED** on the exact
replay head (23/23 jobs passed; a failed-job-only retry cleared one
unrelated ACPX runtime-host timeout).
- Greptile: **5/5** on the exact replay head with zero unresolved review
threads; Superagent, Socket, Snyk, and contributor-trust checks also
passed.
- No local test result is claimed. GitHub Actions is the authoritative
verification environment for this replayed revision.

## Risks

- The validator compiles the embedded response schema for each
submission. Responses are user-paced and bounded, so this keeps the
slice simple without affecting a hot event path.
- The persisted question set is the source of truth for identifiers and
constraints. A malformed persisted set fails closed.
- Numeric input follows the existing structured-question contract,
including JavaScript-prefixed syntax. Optional whitespace-only answers
are rejected instead of being treated as an omitted value.
- Error messages identify the invalid field but do not include answer
text.
- No production path invokes this validator in this pull request.
Request resolution remains the next slice.

> 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, agentic reasoning, tool use, and code
execution.

## 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 and contains no internal
Paperclip ticket id or instance-derived details
- [ ] 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
2026-08-31 02:19:31 -05:00

92 lines
3.2 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json",
"title": "Paperclip question set",
"type": "object",
"required": ["schema", "questions"],
"properties": {
"schema": { "const": "paperclip.question_set.v1" },
"title": { "type": "string", "maxLength": 1000 },
"description": { "type": "string", "maxLength": 4000 },
"submitLabel": { "type": "string", "maxLength": 200 },
"questions": {
"type": "array",
"minItems": 1,
"maxItems": 64,
"items": { "$ref": "#/$defs/question" }
}
},
"$defs": {
"option": {
"type": "object",
"required": ["id", "label"],
"properties": {
"id": { "type": "string", "minLength": 1, "maxLength": 160 },
"label": { "type": "string", "minLength": 1, "maxLength": 1000 },
"description": { "type": "string", "maxLength": 4000 },
"recommended": { "type": "boolean" }
},
"additionalProperties": false
},
"customAnswer": {
"type": "object",
"required": ["enabled"],
"properties": {
"enabled": { "const": true },
"label": { "type": "string", "maxLength": 1000 },
"placeholder": { "type": "string", "maxLength": 1000 }
},
"additionalProperties": false
},
"textValidation": {
"type": "object",
"properties": {
"minLength": { "type": "integer", "minimum": 0, "maximum": 100000 },
"maxLength": { "type": "integer", "minimum": 0, "maximum": 100000 },
"pattern": { "type": "string", "maxLength": 1000 },
"inputType": { "enum": ["text", "number", "integer"] },
"minimum": { "type": "number" },
"maximum": { "type": "number" }
},
"additionalProperties": false
},
"question": {
"type": "object",
"required": ["id", "prompt", "required", "answerMode"],
"properties": {
"id": { "type": "string", "minLength": 1, "maxLength": 160 },
"header": { "type": "string", "maxLength": 1000 },
"prompt": { "type": "string", "minLength": 1, "maxLength": 4000 },
"helpText": { "type": "string", "maxLength": 4000 },
"required": { "type": "boolean" },
"answerMode": { "enum": ["single_select", "multi_select", "text"] },
"options": {
"type": "array",
"maxItems": 128,
"items": { "$ref": "#/$defs/option" }
},
"customAnswer": { "$ref": "#/$defs/customAnswer" },
"textValidation": { "$ref": "#/$defs/textValidation" }
},
"allOf": [
{
"if": { "properties": { "answerMode": { "const": "text" } }, "required": ["answerMode"] },
"then": {
"properties": { "options": { "type": "array", "maxItems": 0 } },
"not": { "required": ["customAnswer"] }
}
},
{
"if": { "properties": { "answerMode": { "enum": ["single_select", "multi_select"] } }, "required": ["answerMode"] },
"then": {
"required": ["options"],
"properties": { "options": { "type": "array", "minItems": 1 } }
}
}
],
"additionalProperties": false
}
},
"additionalProperties": false
}