mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
## 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
92 lines
3.2 KiB
JSON
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
|
|
}
|