Skip to content

fix(archive): support bounded local PAX metadata - #147

Merged
steipete merged 1 commit into
mainfrom
steipete/zealous-proskuriakova-8afae4
Aug 28, 2026
Merged

fix(archive): support bounded local PAX metadata#147
steipete merged 1 commit into
mainfrom
steipete/zealous-proskuriakova-8afae4

Conversation

@steipete

@steipete steipete commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the blanket local-PAX rejection with bounded byte-level interpretation in both JavaScript and native TAR meters. PAX size overrides now drive framing, while effective paths and sizes continue through the existing extraction/read policy. Original archive bytes are not rewritten.

This fixes standard release archives that contain fractional timestamps and binary macOS provenance metadata. The implementation intentionally supports a narrow, documented local-PAX subset: ambiguous records, duplicate keys, global/old headers, mixed extension chains, unsupported structural text, and sparse semantics still fail closed. Xattrs and ownership metadata are accepted only as inert metadata and are not restored. Existing limits, guarded staging, link rejection, and package dependencies are unchanged.

Keep complete-input native validation and terminal cancellation alongside PAX support: the TAR reader stops at end markers and buffers PAX through a retrying read helper. Also return JS TAR-read traversal failures through the public promise. Add native-required PAX coverage to Linux, macOS, Windows, and musl CI lanes.

Verification

  • Genuine red/green: the same initial 12 positive PAX regressions failed with the original rejection, then passed in JavaScript and actual native execution.
  • Fresh public-API live proof: extract and bounded-read the unchanged official Crabbox 0.46.0 Darwin ARM64 archive in both mode: "off" and mode: "require". Download verified at 30,134,912 bytes, SHA-256 2216da0acbcc6e822ee341ec313aaab58875db951fa1daf0d13dd710ebfba9b8. Both extracted member hashes match independent expectations; sizes are 109,941,296 and 6,871,408 bytes, with executable modes retained.
  • Exact metadata/count/member/total limits pass together; reducing each relevant budget by one rejects with the expected typed error and leaves the destination empty. Bounded entry reads pass at the exact size and reject one byte less.
  • Cancellation proof: a 50 ms timeout rejects and the native subprocess exits naturally in 163 ms instead of retrying indefinitely.
  • FS_SAFE_PAX_REQUIRE_NATIVE=1 pnpm check: 1,191 passed, 61 baseline skips, including build/docs/package/public-API checks.
  • Three PAX suites: 146 passed, no skips, including gzip/bzip2/zstd, byte framing, traversal, malformed records, limits, links, and sparse rejection. Wider focused native/boundary run: 229 passed, five platform-specific skips.
  • pnpm native:test: 23 passed; native build, Clippy with warnings denied, workflow lint, and git diff --check passed.

No consumer overrides, cloud credentials/resources, downloaded executable execution, or dependency changes were used. This PR does not publish a package release.

Recorded public-API runtime output

The following is the observed local runtime output from the built public exports for the exact source committed as 2fea6a51a7f6834aa07a01f6f68e1f6c5980fe1a, not an agent transcript. The real archive is unchanged; both native and JavaScript modes are explicit.

Observed extraction and bounded reads: mode off
{
  "phase": "preland",
  "mode": "off",
  "success": true,
  "files": [
    {
      "name": "crabbox",
      "size": 109941296,
      "sha256": "9a4a0b312c26e40e1f4da897b284d6f2ac3f9ac73bbb5e3fe1c957200cf06452"
    },
    {
      "name": "crabbox-apple-vm-helper",
      "size": 6871408,
      "sha256": "ba38d42445d202c843bff099476e115312555733f6d6a8a7a4de0ecbd585c45e"
    }
  ]
}
{
  "phase": "preland",
  "mode": "off",
  "success": true,
  "checks": [
    {
      "check": "read-exact-bytes",
      "name": "crabbox",
      "success": true,
      "size": 109941296,
      "sha256": "9a4a0b312c26e40e1f4da897b284d6f2ac3f9ac73bbb5e3fe1c957200cf06452"
    },
    {
      "check": "read-exact-bytes",
      "name": "crabbox-apple-vm-helper",
      "success": true,
      "size": 6871408,
      "sha256": "ba38d42445d202c843bff099476e115312555733f6d6a8a7a4de0ecbd585c45e"
    },
    {
      "check": "read-over-limit-rejected",
      "success": true
    },
    {
      "check": "extraction-meta-limit-rejected",
      "success": true,
      "code": "archive-meta-entry-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-count-limit-rejected",
      "success": true,
      "code": "archive-entry-count-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-entry-limit-rejected",
      "success": true,
      "code": "archive-entry-extracted-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-total-limit-rejected",
      "success": true,
      "code": "archive-extracted-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-exact-all-limits",
      "success": true
    }
  ]
}
Observed extraction and bounded reads: mode require
{
  "phase": "preland",
  "mode": "require",
  "success": true,
  "files": [
    {
      "name": "crabbox",
      "size": 109941296,
      "sha256": "9a4a0b312c26e40e1f4da897b284d6f2ac3f9ac73bbb5e3fe1c957200cf06452"
    },
    {
      "name": "crabbox-apple-vm-helper",
      "size": 6871408,
      "sha256": "ba38d42445d202c843bff099476e115312555733f6d6a8a7a4de0ecbd585c45e"
    }
  ]
}
{
  "phase": "preland",
  "mode": "require",
  "success": true,
  "checks": [
    {
      "check": "read-exact-bytes",
      "name": "crabbox",
      "success": true,
      "size": 109941296,
      "sha256": "9a4a0b312c26e40e1f4da897b284d6f2ac3f9ac73bbb5e3fe1c957200cf06452"
    },
    {
      "check": "read-exact-bytes",
      "name": "crabbox-apple-vm-helper",
      "success": true,
      "size": 6871408,
      "sha256": "ba38d42445d202c843bff099476e115312555733f6d6a8a7a4de0ecbd585c45e"
    },
    {
      "check": "read-over-limit-rejected",
      "success": true
    },
    {
      "check": "extraction-meta-limit-rejected",
      "success": true,
      "code": "archive-meta-entry-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-count-limit-rejected",
      "success": true,
      "code": "archive-entry-count-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-entry-limit-rejected",
      "success": true,
      "code": "archive-entry-extracted-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-total-limit-rejected",
      "success": true,
      "code": "archive-extracted-size-exceeds-limit",
      "destinationEmpty": true
    },
    {
      "check": "extraction-exact-all-limits",
      "success": true
    }
  ]
}

The automatic review environment could not build its native binding because no Rust toolchain was configured. That is not a runtime result for this change. A working local Rust toolchain built the binding for the results above; all four GitHub native lanes also passed. Build JavaScript before staging the binding (pnpm build, then pnpm native:build), since the JavaScript build clears dist.

@steipete
steipete requested a review from a team as a code owner August 27, 2026 23:26
@clawsweeper

clawsweeper Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 27, 2026
@clawsweeper

clawsweeper Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed August 27, 2026, 7:29 PM ET / 23:29 UTC.

ClawSweeper review

What this changes

This PR adds bounded local PAX TAR metadata support to JavaScript and native archive processing, with documentation, regression coverage, and native CI coverage.

Merge readiness

Blocked until stronger real behavior proof is added - 4 items remain

The bounded PAX implementation is necessary because current main rejects local PAX headers, and the reviewed JavaScript/native paths retain a narrow fail-closed design. The external PR still needs inspectable after-fix runtime evidence, and the current Node 24 failure needs resolution before merge.

Priority: P2
Reviewed head: 2fea6a51a7f6834aa07a01f6f68e1f6c5980fe1a

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The cross-runtime implementation and focused coverage are strong, but inspectable real runtime proof is still required before merge.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: The PR describes an after-fix public-archive run but needs an inspectable redacted terminal transcript, runtime log, recording, screenshot, or linked artifact that shows the observed successful result. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The PR describes an after-fix public-archive run but needs an inspectable redacted terminal transcript, runtime log, recording, screenshot, or linked artifact that shows the observed successful result. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 6 items Current-main behavior: The pinned current-main base rejects local PAX type x headers as unmeterable in both JavaScript and native metadata preflight, so the central behavior is not already implemented.
JavaScript boundary validation: The introduced parser accepts only a defined allowlist, rejects malformed/duplicate records, and limits overrides to path, linkpath, and size before downstream checks.
Native parity: The native meter applies the same bounded PAX state and consumes the effective member size before handing unchanged bytes to the TAR reader.
Findings None None.
Security None None.

Live Verification

Command: pnpm native:build && pnpm build

Result: FAIL (failed) — step 1 run node --input-type=module -e 'import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { extractArchive, readArchiveEntry } from "./dist/archive.js"; import { configureFsSafeNative } from "./dist/native-config.js"; const header=(name,type,size)=›{const h=Buffer.alloc(512);h.write(name);h.write("0000644\\0",100);h.write("0000000\\0",108);h.write("0000000\\0",116);h.write(size.toString(8).padStart(11,"0")+"\\0",124);h.fill(32,148,156);h[156]=type.charCodeAt(0);h.write("ustar\\0"+"00",257);let sum=0;for(const b of h)sum+=b;h.write(sum.toString(8).padStart(6,"0")+"\\0 ",148);return h}; const rec=(k,v)=›{const p=Buffer.from(ˋ ${k}=${v}\\nˋ);let n=p.length+1;while(String(n).length+p.length!==n)n=String(n).length+p.length;return Buffer.concat([Buffer.from(String(n)),p])}; const pad=b=›Buffer.concat([b,Buffer.alloc((512-b.length%512)%512)]); const pax=Buffer.concat([rec("path","renamed"),rec("size","4")]); const tar=Buffer.concat([header("PaxHeader","x",pax.length),pad(pax),header("raw","0",1),pad(Buffer.from("data")),Buffer.alloc(1024)]); const root=await fs.mkdtemp(path.join(os.tmpdir(),"fs-safe-pax-")); try { const archive=path.join(root,"fixture.tar"), out=path.join(root,"out"); await fs.writeFile(archive,tar); await fs.mkdir(out); configureFsSafeNative({mode:"require"}); await extractArchive({archivePath:archive,destDir:out,timeoutMs:10000}); const read=await readArchiveEntry(archive,"renamed",{maxBytes:4}); if ((await fs.readFile(path.join(out,"renamed"),"utf8"))!=="data"||read.toString()!=="data") throw new Error("unexpected PAX result"); console.log("PAX public API extraction and bounded read passed"); } finally { await fs.rm(root,{recursive:true,force:true}); }': terminal run was blocked by the previous command: terminal command failed with exit status 1: "pnpm native:build && pnpm build"

[failure]
terminal run was blocked by the previous command: terminal command failed with exit status 1: "pnpm native:build && pnpm build"

[command 1 combined output]
› @openclaw/fs-safe@0.5.6 native:build /tmp/clawsweeper-live-proof-147-sAOiWR/target
› pnpm --filter @openclaw/fs-safe-native-build build


› @openclaw/fs-safe-native-build@0.5.6 build /tmp/clawsweeper-live-proof-147-sAOiWR/target/native
› napi build --platform --release && node ../scripts/stage-host-native.mjs

Internal Error: cargo metadata exited with code 1 and error message:

error: rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured.
help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain.


error: rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured.
help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain.

    at execCargoMetadata (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/@napi-rs+cli@3.8.6_@emnapi+core@1.11.2_@emnapi+runtime@2.0.0-a
lpha.3_@types+node@26.2.0/node_modules/@napi-rs/cli/dist/cli.js:3038:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async buildProject (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/@napi-rs+cli@3.8.6_@emnapi+core@1.11.2_@emnapi+runtime@2.0.0-
alpha.3_@types+node@26.2.0/node_modules/@napi-rs/cli/dist/cli.js:6806:19)
    at async BuildCommand.execute (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/@napi-rs+cli@3.8.6_@emnapi+core@1.11.2_@emnapi+runtim
e@2.0.0-alpha.3_@types+node@26.2.0/node_modules/@napi-rs/cli/dist/cli.js:12355:20)
    at async BuildCommand.validateAndExecute (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/clipanion@4.0.0-rc.4_typanion@3.14.0/node_
modules/clipanion/lib/advanced/Command.mjs:49:26)
    at async Cli.run (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/clipanion@4.0.0-rc.4_typanion@3.14.0/node_modules/clipanion/lib/ad
vanced/Cli.mjs:227:24)
    at async Cli.runExit (file:///tmp/clawsweeper-live-proof-147-sAOiWR/target/node_modules/.pnpm/clipanion@4.0.0-rc.4_typanion@3.14.0/node_modules/clipanion/li
b/advanced/Cli.mjs:236:28)
/tmp/clawsweeper-live-proof-147-sAOiWR/target/native:
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @openclaw/fs-safe-native-build@0.5.6 build: ˋnapi build --platform --release && node ../scripts/stage-host-native.mjsˋ
Exit status 1
 WARN   Local package.json exists, but node_modules missing, did you mean to install?
 ELIFECYCLE  Command failed with exit code 1.

Assertions:

  • FAIL expect_output: PAX public API extraction and bounded read passed

How this fits together

Archive inputs pass through metadata preflight before JavaScript or native extraction reads entries and writes into guarded destinations. Local PAX metadata can change the next TAR member’s path or framing, so both implementations validate it before existing path, size, and link policies run.

flowchart LR
  A[Archive input] --> B[Metadata preflight]
  B --> C[Bounded local PAX validation]
  C --> D[Effective member framing]
  D --> E[Existing path and limit checks]
  E --> F[Guarded extraction or entry read]
Loading

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The PR describes an after-fix public-archive run but needs an inspectable redacted terminal transcript, runtime log, recording, screenshot, or linked artifact that shows the observed successful result. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Resolve merge risk (P1) - This patch begins accepting metadata that controls TAR member paths and framing at a filesystem security boundary; merge should wait for inspectable after-fix runtime evidence across the JavaScript and required-native paths.
  • Resolve merge risk (P1) - The Node 24 Ubuntu check is currently failing, and the supplied review material has no failure output to establish whether it is unrelated or branch-caused.
  • Complete next step (P2) - This open PR needs contributor-supplied real runtime proof and normal check resolution rather than an automated repair branch.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch footprint 20 files, +1,036 / -41 lines The patch spans both archive implementations, tests, documentation, and CI.
Production versus test delta production +471 / -26, tests +515 / -3 The substantial implementation growth is paired with slightly more focused regression coverage across both runtimes.

Merge-risk options

Maintainer options:

  1. Attach boundary-focused runtime proof (recommended)
    Provide redacted after-fix output for JavaScript-only and required-native extraction/read behavior, then confirm the Node 24 failure is resolved or unrelated.

Technical review

Best possible solution:

Attach a redacted public-API terminal transcript or equivalent artifact showing extraction and bounded entry reads of the cited PAX archive in both JavaScript-only and required-native modes, then resolve the failing Node 24 check before landing.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: pinned current main rejects every local PAX header before either parser can process it, while this PR changes that exact branch. The supplied PR description also names a concrete public archive fixture, but no local execution was performed in this read-only review.

Is this the best way to solve the issue?

Yes, provisionally: bounded byte-level parsing ahead of the existing path, link, byte-budget, and staging checks is a maintainable way to support the needed subset without accepting ambiguous extension chains.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against ed6b6d4b988d.

Labels

Label changes:

  • add P2: This is a bounded archive-format compatibility and correctness change with limited but security-sensitive blast radius.
  • add merge-risk: 🚨 security-boundary: The patch newly accepts attacker-controlled TAR metadata that affects effective paths and member sizes before filesystem extraction.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR describes an after-fix public-archive run but needs an inspectable redacted terminal transcript, runtime log, recording, screenshot, or linked artifact that shows the observed successful result. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P2: This is a bounded archive-format compatibility and correctness change with limited but security-sensitive blast radius.
  • merge-risk: 🚨 security-boundary: The patch newly accepts attacker-controlled TAR metadata that affects effective paths and member sizes before filesystem extraction.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR describes an after-fix public-archive run but needs an inspectable redacted terminal transcript, runtime log, recording, screenshot, or linked artifact that shows the observed successful result. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

  • Current-main behavior: The pinned current-main base rejects local PAX type x headers as unmeterable in both JavaScript and native metadata preflight, so the central behavior is not already implemented. (src/archive-tar-meta.ts:68, ed6b6d4b988d)
  • JavaScript boundary validation: The introduced parser accepts only a defined allowlist, rejects malformed/duplicate records, and limits overrides to path, linkpath, and size before downstream checks. (src/archive-tar-pax.ts:24, 2fea6a51a7f6)
  • Native parity: The native meter applies the same bounded PAX state and consumes the effective member size before handing unchanged bytes to the TAR reader. (native/src/tar_meter.rs:124, 2fea6a51a7f6)
  • Focused regression coverage: The introduced tests cover path traversal, malformed records, metadata and extraction limits, compressed native formats, links, and JS/native PAX behavior. (test/archive-pax-security.test.ts:36, 2fea6a51a7f6)
  • Feature history: Archive policy and metadata-preflight work on current history is concentrated in Peter Steinberger's commits, including the prior parser hardening work. (native/src/tar_meter.rs:1, ed06666a28eb)
  • Real-behavior proof assessment: The PR body describes a detailed live run against an official archive, but it does not attach a terminal transcript, redacted log, screenshot, recording, or artifact showing the after-fix observed result. (2fea6a51a7f6)

Likely related people:

  • steipete: Peter Steinberger authored the current archive/PAX work and prior merged archive policy and parser-hardening commits in the same paths. (role: archive feature owner and recent area contributor; confidence: high; commits: b1be2a119986, ed06666a28eb, 2fea6a51a7f6; files: src/archive-tar-meta.ts, native/src/tar_meter.rs, native/src/archive.rs)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Attach redacted public-API output proving extraction and bounded reads in mode off and mode require.
  • Resolve the current Node 24 Ubuntu failure or attach its diagnosis showing it is unrelated.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper

clawsweeper Bot commented Aug 28, 2026

Copy link
Copy Markdown

ClawSweeper status: review started.

I am starting a fresh review of this pull request: fix(archive): support bounded local PAX metadata This is item 1/1 in the current shard. Shard 0/1.

This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking.

Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted.

@steipete
steipete merged commit 8850a24 into main Aug 28, 2026
46 of 48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant