mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 03:08:10 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The adapter layer carries sandbox requests to host processes. > - The HTTP/2 bridge used one process-wide byte ledger for all routes. > - One busy route could exhaust that shared budget and move another route to file transport. > - This pull request gives each host retention site a fixed byte bound and limits concurrent HTTP/2 streams. > - The benefit is local protection: one route cannot consume the byte budget of another route. ## Linked Issues or Issue Description **What happened?** The HTTP/2 bridge used one aggregate byte ledger for retained bytes across all routes. A busy route could exhaust the shared budget and force an unrelated route to use file transport. **Expected behavior** Each route should protect its own retained bytes. A reset on one HTTP/2 stream should cancel only that stream's host forward. **Steps to reproduce** 1. Start the HTTP/2 bridge with multiple sandbox routes. 2. Send enough retained data through one route to reach the aggregate byte limit. 3. Send a request through a sibling route. 4. Observe that the sibling route can fall back to file transport because the first route used the shared ledger. **Paperclip version or commit** `47639e227e78e3c5e0dd1a3c0e2d792fe86895a3` **Deployment mode** Built from source with the adapter-utils and server test suites. ## What Changed - Bound each host retention site with a fixed local byte limit. - Limited concurrent live HTTP/2 streams with one built-in stream limit. - Bound each host forward and response-body read to its own HTTP/2 stream lifetime. - Removed the process-wide byte ledger, its environment override, its metrics, and its file-transport fallbacks. - Added tests for the stream limit, host body budget, and sibling-stream cancellation. ## Verification - Run `pnpm vitest run --project adapter-utils`. - Confirm that 996 adapter-utils tests pass. - Confirm that `test_live_forward_work_never_passes_the_stream_limit` passes. - Confirm that `test_the_host_body_budget_matches_the_stream_limit` passes. - Confirm that the sibling-stream cancellation test passes. - Run `pnpm tsc --noEmit`. - Confirm that all pull request checks pass. ## Risks The bridge no longer uses a process-wide byte ledger. A local bound or stream limit that is too low can reject or delay valid work. The tests cover the new limits and stream cancellation behavior. ## Model Used OpenAI GPT-5 Codex. Runtime model ID: GPT-5. The model used code execution and repository tools. The runtime does not expose the context window size. ## 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 - [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>
208 lines
5.6 KiB
JSON
208 lines
5.6 KiB
JSON
{
|
|
"frameVersion": 2,
|
|
"defaultMaxFrameBytes": 262144,
|
|
"description": "Shared wire-compatibility vectors for the duplex frame codec. Every codec copy decodes the same bytes. bytes is a UTF-8 byte stream; expected lists the decode result (frame or protocol-error code).",
|
|
"vectors": [
|
|
{
|
|
"name": "valid-ready",
|
|
"category": "valid",
|
|
"bytes": "{\"version\":2,\"type\":\"ready\",\"nonce\":\"9f8e7d6c5b4a39281706f5e4d3c2b1a0\"}\n",
|
|
"roundTrip": true,
|
|
"expected": [
|
|
{
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "ready",
|
|
"nonce": "9f8e7d6c5b4a39281706f5e4d3c2b1a0"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "valid-heartbeat",
|
|
"category": "valid",
|
|
"bytes": "{\"version\":2,\"type\":\"heartbeat\"}\n",
|
|
"roundTrip": true,
|
|
"expected": [
|
|
{
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "heartbeat"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "valid-close",
|
|
"category": "valid",
|
|
"bytes": "{\"version\":2,\"type\":\"close\"}\n",
|
|
"roundTrip": true,
|
|
"expected": [
|
|
{
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "close"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "valid-error",
|
|
"category": "valid",
|
|
"bytes": "{\"version\":2,\"type\":\"error\",\"code\":\"malformed_frame\",\"message\":\"peer reported a malformed frame\"}\n",
|
|
"roundTrip": true,
|
|
"expected": [
|
|
{
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "error",
|
|
"code": "malformed_frame",
|
|
"message": "peer reported a malformed frame"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-ready-missing-nonce",
|
|
"category": "invalid",
|
|
"bytes": "{\"version\":2,\"type\":\"ready\"}\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-ready-with-address",
|
|
"category": "invalid",
|
|
"bytes": "{\"version\":2,\"type\":\"ready\",\"nonce\":\"9f8e7d6c5b4a39281706f5e4d3c2b1a0\",\"address\":\"http://127.0.0.1:47215\"}\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-ready-with-port",
|
|
"category": "invalid",
|
|
"bytes": "{\"version\":2,\"type\":\"ready\",\"nonce\":\"9f8e7d6c5b4a39281706f5e4d3c2b1a0\",\"port\":47215}\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-not-json",
|
|
"category": "invalid",
|
|
"bytes": "{not valid json\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-json-array",
|
|
"category": "invalid",
|
|
"bytes": "[1,2,3]\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-json-number",
|
|
"category": "invalid",
|
|
"bytes": "42\n",
|
|
"expected": [
|
|
{
|
|
"error": "malformed_frame"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "invalid-unknown-type",
|
|
"category": "invalid",
|
|
"bytes": "{\"version\":2,\"type\":\"teleport\"}\n",
|
|
"expected": [
|
|
{
|
|
"error": "unknown_type"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "version-mismatch-lower",
|
|
"category": "versionMismatch",
|
|
"bytes": "{\"version\":1,\"type\":\"request\",\"id\":\"req-1\",\"method\":\"GET\",\"path\":\"/api/issues/PAP-1\",\"query\":\"expand=comments\",\"headers\":{\"accept\":\"application/json\",\"content-type\":\"application/json\"},\"bodyByteCount\":0}\n",
|
|
"expected": [
|
|
{
|
|
"error": "version_mismatch"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "version-mismatch-higher",
|
|
"category": "versionMismatch",
|
|
"bytes": "{\"version\":3,\"type\":\"request\",\"id\":\"req-1\",\"method\":\"GET\",\"path\":\"/api/issues/PAP-1\",\"query\":\"expand=comments\",\"headers\":{\"accept\":\"application/json\",\"content-type\":\"application/json\"},\"bodyByteCount\":0}\n",
|
|
"expected": [
|
|
{
|
|
"error": "version_mismatch"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "version-mismatch-missing",
|
|
"category": "versionMismatch",
|
|
"bytes": "{\"type\":\"heartbeat\"}\n",
|
|
"expected": [
|
|
{
|
|
"error": "version_mismatch"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"encodeDescription": "Encode-bound vectors. Each codec copy runs the size-checked encode against the frame with the maxFrameBytes limit, then asserts the same result. An ok result must decode back to the same frame. This proves both copies enforce the same bound on encode.",
|
|
"encodeVectors": [
|
|
{
|
|
"name": "encode-within-bound",
|
|
"maxFrameBytes": 200,
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "error",
|
|
"code": "read_timeout",
|
|
"message": "upstream did not respond"
|
|
},
|
|
"expected": {
|
|
"ok": true
|
|
}
|
|
},
|
|
{
|
|
"name": "encode-control-frame-never-rejected",
|
|
"maxFrameBytes": 200,
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "heartbeat"
|
|
},
|
|
"expected": {
|
|
"ok": true
|
|
}
|
|
},
|
|
{
|
|
"name": "encode-over-bound",
|
|
"maxFrameBytes": 200,
|
|
"frame": {
|
|
"version": 2,
|
|
"type": "error",
|
|
"code": "read_timeout",
|
|
"message": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
},
|
|
"expected": {
|
|
"ok": false,
|
|
"error": "frame_too_large"
|
|
}
|
|
}
|
|
]
|
|
}
|