Repository navigation
Add standalone Paperclip task exporter - #17077
noverhead-germany wants to merge 1 commit into
Conversation
[Medium impact] The exporter copies one task's records and run history into a local archive. Do not merge until credentials are protected and exports reliably preserve the requested task and its records. Findings
Fix with agent prompt### Issue 1
tools/PaperclipExport/Program.cs:15-16
The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.
**How this was verified:** Every API request uses the HTTP `BaseUrl` with an `Authorization: Bearer` header, and the server accepts the token until it expires or is revoked.
### Issue 2
tools/PaperclipExport/Program.cs:7
`ConfigPath` points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at `File.ReadAllTextAsync` before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.
```suggestion
var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json");
```
### Issue 3
tools/PaperclipExport/Program.cs:22-24
Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch `/api/issues/{ticket}` directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.
### Issue 4
tools/PaperclipExport/Program.cs:33
Comments, run events, and logs are saved after just one page:
- Tasks with more than 100 comments lose later comments.
- The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.
These successful responses add no gap to `ERRORS.txt`. Follow the comment cursor, event `seq`, and log `nextOffset` until all pages have been saved.
### Issue 5
tools/PaperclipExport/Program.cs:32
Attachment downloads require an absolute `url`, but the API returns a relative `contentPath` instead. The download branch therefore skips every attachment without recording an error. Fetch the returned `contentPath` and use `originalFilename` for the archive entry.
### Issue 6
tools/PaperclipExport/Program.cs:37
A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only `ERRORS.txt`, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds a standalone .NET 10 exporter that collects task records into a ZIP.
noverhead-germany explicitly states that the API base URL and company ID are fixed for the current deployment. Their lack of configurability was not reported. Reviews (1) · Last reviewed commit: "Add standalone Paperclip task exporter" · Reviewed by Greptile |
| using var http = new HttpClient { BaseAddress = new Uri(BaseUrl), Timeout = TimeSpan.FromSeconds(45) }; | ||
| http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); |
There was a problem hiding this comment.
Login token travels unencrypted
The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.
How this was verified: Every API request uses the HTTP BaseUrl with an Authorization: Bearer header, and the server accepts the token until it expires or is revoked.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 15-16
Comment:
**Login token travels unencrypted**
The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.
**How this was verified:** Every API request uses the HTTP `BaseUrl` with an `Authorization: Bearer` header, and the server accepts the token until it expires or is revoked.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| using System.Text.RegularExpressions; | ||
|
|
||
| const string BaseUrl = "http://192.168.1.21:3100"; | ||
| const string ConfigPath = @"C:\Users\NOverhead\.paperclip\auth.json"; |
There was a problem hiding this comment.
Other users cannot load credentials
ConfigPath points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at File.ReadAllTextAsync before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.
| const string ConfigPath = @"C:\Users\NOverhead\.paperclip\auth.json"; | |
| var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json"); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 7
Comment:
**Other users cannot load credentials**
`ConfigPath` points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at `File.ReadAllTextAsync` before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.
```suggestion
var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json");
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| var list=await JsonGet($"/api/companies/{CompanyId}/issues?limit=500&offset=0", "Issue lookup"); JsonElement issue=default; | ||
| if(list is not null){var root=list.RootElement;IEnumerable<JsonElement> items=root.ValueKind==JsonValueKind.Array?root.EnumerateArray():root.TryGetProperty("items",out var it)&&it.ValueKind==JsonValueKind.Array?it.EnumerateArray():[];issue=items.FirstOrDefault(x=>x.TryGetProperty("identifier",out var v)&&string.Equals(v.GetString(),ticket,StringComparison.OrdinalIgnoreCase));} | ||
| if(issue.ValueKind!=JsonValueKind.Object){gaps.Add($"Issue {ticket} was not found in the first 500 company issues.");} |
There was a problem hiding this comment.
Tasks beyond page one disappear
Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch /api/issues/{ticket} directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 22-24
Comment:
**Tasks beyond page one disappear**
Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch `/api/issues/{ticket}` directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| await Save("documents.json",await Get($"/api/issues/{id}/documents","Documents")); | ||
| var attachments=await JsonGet($"/api/issues/{id}/attachments","Attachments"); | ||
| if(attachments is not null){await Save("attachments.json",JsonSerializer.SerializeToUtf8Bytes(attachments.RootElement,new JsonSerializerOptions{WriteIndented=true}));IEnumerable<JsonElement> arr=attachments.RootElement.ValueKind==JsonValueKind.Array?attachments.RootElement.EnumerateArray():[];var ix=0;foreach(var a in arr){ix++;if(a.TryGetProperty("url",out var u)&&u.ValueKind==JsonValueKind.String&&Uri.TryCreate(u.GetString(),UriKind.Absolute,out var uri)&&uri.Host==new Uri(BaseUrl).Host)await Save($"attachments/{ix}-{Path.GetFileName(uri.LocalPath)}",await Get(uri.PathAndQuery,$"Attachment {ix} file"));}} | ||
| var runs=await JsonGet($"/api/issues/{id}/runs","Agent runs for logs"); if(runs is not null){IEnumerable<JsonElement> rs=runs.RootElement.ValueKind==JsonValueKind.Array?runs.RootElement.EnumerateArray():runs.RootElement.TryGetProperty("items",out var ri)&&ri.ValueKind==JsonValueKind.Array?ri.EnumerateArray():[];foreach(var run in rs){if(!(run.TryGetProperty("runId",out var rv)||run.TryGetProperty("id",out rv))||rv.GetString() is not string rid)continue;await Save($"runs/{rid}/events.json",await Get($"/api/heartbeat-runs/{rid}/events?afterSeq=0&limit=10000",$"Run {rid} events"));await Save($"runs/{rid}/log.json",await Get($"/api/heartbeat-runs/{rid}/log?offset=0&limitBytes=10000000",$"Run {rid} log"));}} |
There was a problem hiding this comment.
Longer histories are cut short
Comments, run events, and logs are saved after just one page:
- Tasks with more than 100 comments lose later comments.
- The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.
These successful responses add no gap to ERRORS.txt. Follow the comment cursor, event seq, and log nextOffset until all pages have been saved.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 33
Comment:
**Longer histories are cut short**
Comments, run events, and logs are saved after just one page:
- Tasks with more than 100 comments lose later comments.
- The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.
These successful responses add no gap to `ERRORS.txt`. Follow the comment cursor, event `seq`, and log `nextOffset` until all pages have been saved.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| await Save("runs.json",await Get($"/api/issues/{id}/runs","Agent runs")); | ||
| await Save("documents.json",await Get($"/api/issues/{id}/documents","Documents")); | ||
| var attachments=await JsonGet($"/api/issues/{id}/attachments","Attachments"); | ||
| if(attachments is not null){await Save("attachments.json",JsonSerializer.SerializeToUtf8Bytes(attachments.RootElement,new JsonSerializerOptions{WriteIndented=true}));IEnumerable<JsonElement> arr=attachments.RootElement.ValueKind==JsonValueKind.Array?attachments.RootElement.EnumerateArray():[];var ix=0;foreach(var a in arr){ix++;if(a.TryGetProperty("url",out var u)&&u.ValueKind==JsonValueKind.String&&Uri.TryCreate(u.GetString(),UriKind.Absolute,out var uri)&&uri.Host==new Uri(BaseUrl).Host)await Save($"attachments/{ix}-{Path.GetFileName(uri.LocalPath)}",await Get(uri.PathAndQuery,$"Attachment {ix} file"));}} |
There was a problem hiding this comment.
Attachment downloads require an absolute url, but the API returns a relative contentPath instead. The download branch therefore skips every attachment without recording an error. Fetch the returned contentPath and use originalFilename for the archive entry.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 32
Comment:
**Attached files are missing**
Attachment downloads require an absolute `url`, but the API returns a relative `contentPath` instead. The download branch therefore skips every attachment without recording an error. Fetch the returned `contentPath` and use `originalFilename` for the archive entry.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Failed exports overwrite useful copies
A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only ERRORS.txt, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 37
Comment:
**Failed exports overwrite useful copies**
A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only `ERRORS.txt`, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Thinking Path
Linked Issues or Issue Description
Problem or motivation
Operators need a portable ZIP of one task and its related records.
Proposed solution
Provide a standalone .NET 10 Windows executable that reads local Paperclip credentials and exports task data to a ZIP archive.
Alternatives considered
Manual API requests require repeated setup and do not produce a single archive.
What Changed
ERRORS.txt.Verification
dotnet publish tools/PaperclipExport/PaperclipExport.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=trueRisks
Model Used
Checklist