Skip to content

feat(tcfeed): budget the account, and refresh the requests already sent - #122

Merged
ralyodio merged 1 commit into
masterfrom
worktree-tcfeed-throttle
Aug 13, 2026
Merged

feat(tcfeed): budget the account, and refresh the requests already sent#122
ralyodio merged 1 commit into
masterfrom
worktree-tcfeed-throttle

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Refs #120. The rail that was missing when 33 requests went out in one day.

The gap

TCFEED_PR_MAX bounds a run. Nothing bounded the account — and every one of those 33 runs was individually inside its cap. That is the whole difference between a per-run cap and a budget.

prCommand's own docstring already said what it costs:

Bulk unsolicited pull requests are against GitHub's acceptable use policy however good the workflow is, and an account that sends them stops being able to send anything.

This makes that paragraph executable instead of advisory.

Two numbers, because they fail differently

default what it bounds
TCFEED_PR_STANDING 30 unanswered requests standing at once — the footprint. 33 open requests is what "bulk" looks like to a human reading the account, whenever they were sent.
TCFEED_PR_PER_DAY 20 requests opened in a rolling 24h — the velocity. 33 in one afternoon reads as automation even if the total is modest. 20 matches TCFEED_PR_MAX, so one full --all run is a day of sending.

Rolling, not since-midnight: the point is velocity, and midnight resets it to zero for an account that sent thirty at 23:00.

Counted with search/issues total_count rather than by listing, because listing caps at 100 and a budget that silently undercounts once the number gets interesting is worse than no budget at all. A count that cannot be read stops the run rather than defaulting to zero — defaulting to zero would open the floodgates on precisely the failure this exists to catch.

Checked after the run's own cap and before anything opens. Dry runs are exempt (they open nothing) but still print the standing, which is the number worth seeing before deciding to send.

Verified against the live account

Refuses when over budget:

$ tcfeed pr profullstack/threatcrush
standing: 24 unanswered of 30, 33 opened in the last day of 20

tcfeed: no room to send. Nothing was opened.
  `tcfeed check` shows where the open ones stand; the budget frees up as
  they are answered, and the rolling day frees up on its own.
  TCFEED_PR_STANDING and TCFEED_PR_PER_DAY raise it if you mean to.

Trims rather than refuses when there is partial room, and says so — a run that quietly did two of three reads exactly like a run that did three:

$ TCFEED_PR_PER_DAY=40 TCFEED_PR_STANDING=27 tcfeed pr <three repos>
standing: 25 unanswered of 27, 33 opened in the last day of 40
  room for 2 this run; 1 left for later.

Dry run reports and proceeds:

$ tcfeed pr profullstack/threatcrush --dry-run
standing: 24 unanswered of 30, 33 opened in the last day of 20
· profullstack/threatcrush — skipped: already has the workflow

tsc --strict --noEmit clean.

Note on the current standing

With defaults, this still refuses to send anything today — 33 went out in the last 24h against a cap of 20, so the account is 13 over. Standing has room for 6. The daily window clears on its own; the standing budget frees up as maintainers answer. Both env vars raise it deliberately, which is the point: going over should be a thing someone typed.

🤖 Generated with Claude Code


Also: refresh the requests already sent

The pack is read live from sh1pt so a corrected workflow reaches repositories — but that was only ever true of requests not yet sent. 33 went out carrying npm install -g "…@latest", the pack was fixed an hour later, and all 24 still open kept showing the defective version to anyone who looked.

Three reviewers found it independently:

  • SonarCloud — failed a quality gate, githubactions:S8543

  • CodeRabbit — scored a request 🟡 Moderate for "an unpinned scanner with access to a write-scoped job"

  • GlassHaven/Haven#532 — declined on exactly that ground:

    whoever can publish that package can run code in this repository's CI from that point on, forever, without a further PR

    noting the project hash-pins 13 tarballs in its ffmpeg stack alone. That is a maintainer applying their own published bar to us and finding us under it.

tcfeed check --refresh rewrites both files on every open request to whatever the pack currently renders, and reports already current when there is nothing to do — so it is safe to run after any pack change without churning commits.

Written against the contents API rather than a clone: the open requests include seastar and lightning, and cloning a kernel-sized repository to rewrite two files under .github is minutes of transfer for a diff that fits on a screen. It also cannot carry anything else along by accident, which on somebody else's review is the more important half.

Run against all 24 open requests — 22 updated, 2 already current, verified independently on lightning and maltrail:

$ tcfeed check --refresh
· ElementsProject/lightning — updated threatcrush-scan.yml
· stamparm/maltrail — updated threatcrush-scan.yml
  ... 20 more ...
· NeptuneHub/AudioMuse-AI — already current
· scylladb/seastar — already current

$ gh api .../ralyodio/lightning/contents/...threatcrush-scan.yml | base64 -d | grep 'npm install -g'
            if npm install -g --ignore-scripts "@profullstack/threatcrush@0.11.0"; then

Idempotency checked by running it twice on the same repo: second run reports already current and pushes nothing.

Refs #120.

TCFEED_PR_MAX bounds a run. Nothing bounded the account, and on the first
day that mattered: 33 unsolicited pull requests went out across 33
strangers' repositories in a single day, 25 still unanswered afterwards.
Every run was individually inside its cap. That is the difference between
a per-run cap and a budget, and prCommand's own docstring already said
what it costs — bulk unsolicited pull requests are against GitHub's
acceptable use policy however good the workflow is, and an account that
sends them stops being able to send anything.

budget() makes that paragraph executable. Two numbers, because they fail
differently:

  TCFEED_PR_STANDING  unanswered requests allowed to stand at once,
                      default 30. The footprint — 33 open requests is what
                      "bulk" looks like to a human reading the account,
                      whenever they were sent.
  TCFEED_PR_PER_DAY   requests opened in a rolling 24 hours, default 10.
                      The velocity — 33 in one afternoon reads as
                      automation even if the total is modest.

Rolling, not since-midnight: the point is velocity, and midnight resets
it to zero for an account that sent thirty at 23:00.

Counted with search/issues total_count rather than by listing, because
listing caps at 100 and a budget that silently undercounts once the
number gets interesting is worse than no budget. A count that cannot be
read stops the run rather than defaulting to zero, which would open the
floodgates on exactly the failure this catches.

Checked after the run's own cap and before anything opens. Dry runs are
exempt because they open nothing, but still print the standing, which is
the number worth seeing before deciding to send.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

67 finding(s)

HIGH/CRITICAL: 11 | MEDIUM: 55 | LOW: 1

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH sh-eval-expansion .githooks/pre-commit:26
HIGH sh-remote-script-execution apps/web/public/install.sh:272
HIGH sh-remote-script-execution apps/web/public/install.sh:320
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH secret-generic-credential PRD.md:268
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
HIGH sh-remote-script-execution scripts/smoke-test.sh:46
HIGH sh-remote-script-execution scripts/smoke-test.sh:47
MEDIUM insecure-temp-file .githooks/commit-msg:16
MEDIUM insecure-temp-file .githooks/post-commit:20
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:88
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:111
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:121
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:125
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:419
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:184
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:153
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:157
MEDIUM js-unescaped-html-sink apps/web/src/app/get-whitepaper/page.tsx:346
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:211
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:215
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:120
MEDIUM js-unescaped-html-sink apps/web/src/app/store/[slug]/page.tsx:107
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM manifest-install-lifecycle-script package.json:24

…and 17 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit bd299a5 into master Aug 13, 2026
11 of 12 checks passed
@ralyodio ralyodio changed the title feat(tcfeed): budget the account, not just the run feat(tcfeed): budget the account, and refresh the requests already sent Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant