chore(release): promote next to production (2026-06-01)#528
chore(release): promote next to production (2026-06-01)#528hello-happy-puppy wants to merge 12 commits into
Conversation
…anifest (#517) Sync the bot and user scope arrays in createOAuthHandler() with the current Slack app manifest so granted tokens carry all configured permissions. Bot scopes: 14 to 15 (adds incoming-webhook). User scopes: 14 to 39 (adds search:read family, canvases, bookmarks, links, reminders, pins, files read/write, emoji:read, channels:write, groups:write, mpim:write, users:read.email). Existing tokens retain their previous narrower scopes; users must re-authorize to receive the new permissions.
The scope was added in PR #517 to mirror the Slack app manifest, but the OAuth callback handler discards tokenResponse.incoming_webhook (only forwards team, enterprise, scopes, bot_user_id). The app does not consume webhook URLs, so requesting the scope adds a channel- picker step to install for no functional benefit. Manifest should be updated separately to remove incoming-webhook from oauth_config.scopes.bot so the two stay aligned.
#520) * chore(slack-oauth-backend): log OAuth request scopes and Slack-granted scopes Adds two diagnostic logger.info calls to surface what we ask Slack for and what Slack returns on each install, so future scope-grant mismatches can be debugged against Vercel logs instead of trial-and-error reinstalls. handler.ts (after exchangeCodeForToken): logs botScopesGranted, userScopesGranted, presence flags for bot/user access and refresh tokens, token types, team id, enterprise id. Token values themselves are not logged. routes/oauth.ts (in /authorize handler): logs the full redirect URL including scope and user_scope query params, so we can verify the request side independently of Slack's response. Temporary instrumentation for the ongoing org-install debugging; safe to leave in but a follow-up can prune if these become noisy. * chore(slack-oauth-backend): scope-only log + drop "Debug" framing Addresses two review comments on #520: routes/oauth.ts: parse the authorize URL and log only the scope, user_scope, and redirect_uri params instead of the full URL. The full URL contains the `state` CSRF nonce, which would otherwise leak into Vercel logs and weaken the CSRF protection validated on callback. handler.ts + routes/oauth.ts: drop the "Debug:" framing from both log comments. The logs fire at info level in production and the data they capture (scope strings, presence flags) is permanent OAuth observability, not temporary instrumentation, so the comments now reflect that intent.
…ce (#513) _claude-docs-check.yml's Build Docs Check Prompt step substitutes 8 placeholders into the prompt template via `sed -i "s|\${VAR}|$VALUE|g"` with `|` as the sed delimiter. The escape applied to $CHANGED_FILES on line 520 is `sed 's/[&/\]/\\&/g'` — it escapes `&`, `/`, `\` but NOT `|` (the actual delimiter). A filename in $CHANGED_FILES containing `|` (legal on Linux; not quoted by git's default core.quotepath since `|` is in the printable ASCII range) breaks the substitution into garbage flags and causes the docs-check workflow to fail or write a partial value into the prompt. PR authors can introduce such filenames via fork PRs. Replace the 8 sed -i calls with a single python str.replace pass. Python's str.replace is plain-text and immune to this entire class of delimiter-injection bugs. Discovered during the bug bounty review that produced Uniswap/default-token-list#2484 and #509. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): harden Claude session in task worker (drop --dangerously-skip-permissions, bullfrog block) The autonomous task worker had two structural gaps that any prompt- injection bug would weaponize: 1. Claude ran with `--dangerously-skip-permissions`, meaning every tool call was approved automatically. Combined with the absence of a file-allowlist post-flight check (see the separate Validate-changed- files PR), a prompt-injected session could call any tool unrestricted. 2. bullfrog egress-policy was `audit` (log-only). Network calls to attacker-controlled endpoints would be logged but not blocked. This patch: a) Replaces `--dangerously-skip-permissions` with an explicit `--allowedTools` list. Without an allowlist Claude prompts on every tool call (no human to approve in CI, so the job stalls); with the explicit allowlist the worker can still operate but a hijacked session cannot call arbitrary tools (curl, wget for exfil; dangerous mcp__linear__ operations like delete_*; etc.). Refine the list as legitimate tasks reveal additional needed tools. b) Switches bullfrog from `egress-policy: audit` to `egress-policy: block` with an allowlist matching the other ai-toolkit workflows that already run in block mode (_claude-main.yml, _claude-code-review.yml, _claude-docs-check.yml, _generate-pr-metadata.yml), plus api.linear.app for the Linear MCP server this worker uses. A follow-up that this patch does NOT address: the GitHub App's installation permissions are configured server-side and are out of scope for a code PR. Audit the App's scopes in the GitHub UI and reduce to the minimum needed (contents:write, pull_requests:write, issues:read). Discovered during the bug bounty review that produced Uniswap/default-token-list#2484 and #509. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workflows): tighten bullfrog allowlist to aitk-worker-specific empirically-derived hosts The previous commit shipped a 10-entry allowlist matching the dtl worker's set, but the aitk Task Worker has different needs (uses registry.npmjs.org as its npm registry, not registry.yarnpkg.com) and the shared list included entries unused in practice. Per-workflow analysis based on 15 audit-mode runs of _generate-pr-metadata.yml (the only ai-toolkit workflow with recent runs and audit logs showing the same setup-bun + bun install pattern) plus reading this worker's code: Setup-phase empirical: - registry.npmjs.org (341 audit lines across 15 sister runs) - release-assets.githubusercontent.com (covered by *.githubusercontent.com) Runtime (invisible to audit per agent/queue_audit.nft — DNS reply queue means cached resolutions emit no further events): - api.anthropic.com (claude-code-action) - api.linear.app (mcp__linear__ tools at runtime) Default-allowed by bullfrog (agent/agent.go:18 defaultDomains): - github.com, api.github.com — silently allowed Dropped vs the previous shared allowlist: - github.com / *.github.com — covered by defaultDomains - bun.sh — 0 audit lines in 15 setup-bun runs; the binary comes from github releases via *.githubusercontent.com - claude.ai / *.claude.ai — OAuth path only; this worker uses ANTHROPIC_API_KEY Final allowlist: 5 entries (down from 10), each empirically justified or required by code that audit logs can't observe. Sister PR Uniswap/default-token-list#2485 makes the analogous tightening for the dtl Task Worker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(claude-task-worker): clarify allowlist limits + add Bash(rg:*) Rewords the comment above allowedTools to stop overstating what the list contains. node/python3/bunx/npx are all arbitrary code execution; the real containment is the bullfrog egress block above. Documents that explicitly so a future maintainer doesn't loosen the bullfrog allowlist assuming the tool allowlist constrains exfiltration — it does not. Also adds Bash(rg:*) preemptively per the PR's own 'add the tool here' guidance: Claude's default search flows prefer ripgrep and would otherwise stall on a permission prompt mid-task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…511) * fix(workflows): set BUN_CONFIG_FILE=/dev/null to neutralize bunfig.toml.preload RCE `bun run`, `bun install`, and `bun x` auto-load bunfig.toml from CWD. The bunfig.toml.preload array executes arbitrary code before the intended script. When a workflow checks out PR head content (any of our reusable workflows that take a pr_number input do this), `bun run` loads bunfig.toml from the checkout root, and a malicious preload entry executes with the caller's secrets. Disable bunfig.toml loading by setting BUN_CONFIG_FILE=/dev/null at the workflow level. This propagates to all jobs and steps and covers every `bun *` invocation in the workflow. Affected files (all use `bun run`/`bun install` after checkout): - _claude-docs-check.yml - _claude-code-review.yml - _claude-task-worker.yml - ci-pr-checks.yml - publish-packages.yml Discovered during the bug bounty review that produced Uniswap/default-token-list#2484 and #509 (semgrep flagged 8+ sites; this fix is uniform across all of them). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workflows): also prefix BUN_CONFIG_FILE=/dev/null inline on bun run Belt-and-suspenders: job env already sets BUN_CONFIG_FILE=/dev/null, but pinning it inline on the bun run invocation matches the Semgrep CI comment's suggested fix and survives anyone later moving this into a step that overrides the job env. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): validate toolkit_ref against allowlist before downloading scripts All six reusable workflow_call workers (_claude-task-worker.yml, _claude-code-review.yml, _claude-docs-check.yml, _claude-main.yml, _generate-pr-metadata.yml, _update-action-versions-worker.yml) accept a `toolkit_ref` input and use it directly in curl URLs to fetch action.yml / post-*.ts script content from this repository: curl -L "https://api.github.com/repos/Uniswap/ai-toolkit/contents/.github/.../action.yml?ref=${TOOLKIT_REF}" The downloaded content is then executed inside the worker job with access to its secrets (ANTHROPIC_API_KEY, LINEAR_API_KEY, GitHub App installation tokens). If `toolkit_ref` is set to an attacker-controlled ref — e.g., a fork branch — they get RCE-with-secrets the moment any of these workflows is invoked with the attacker's branch. Add a "Validate toolkit_ref" step as the FIRST step in each worker job. Allowlist: main, next, or a 40-char SHA. Anything else fails the job with a clear error. The validation runs before the bullfrog egress scan (which is the next step) so even if the egress allowlist is misconfigured, attacker- controlled refs are rejected before any network call is made. Discovered during the bug bounty review that produced Uniswap/default-token-list#2484 and #509. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workflows): pin claude-docs-check toolkit_ref to main This caller previously passed `toolkit_ref: ${{ github.head_ref || github.ref_name }}`, which resolves to the PR branch name on every pull_request: run. The downstream worker (_claude-docs-check.yml) curls action.yml / post-*.ts script content from `Uniswap/ai-toolkit@$TOOLKIT_REF` and executes it inside the worker job with access to the caller's secrets (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, WORKFLOW_PAT) and the GitHub App installation token. A PR author could therefore land malicious script content on their own branch and have the docs-check workflow execute it with full secrets. For internal-author PRs (anyone with push access to this repo) the attack is complete RCE-with-secrets; for fork PRs the blast is bounded by what `pull_request:` flows expose, but the pattern is brittle either way. Sequencing: this PR should land BEFORE the toolkit_ref allowlist validation in #511. After this lands, #511's validation step has no in-repo caller that would fail the check. Landing #511 first without this PR would break docs-check on every non-main/next branch. Trade-off accepted: PR authors who modify worker scripts (post-docs-check.ts, validate-claude-auth/action.yml, etc.) can no longer self-test their changes via docs-check on the same PR. Test flow is now: 1. Land the script change on `next` first 2. Verify docs-check works on next 3. Promote next → main via the existing release-notes flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workflows): move toolkit_ref validation after bullfrog per CLAUDE.md The 'bullfrog must be first step' rule in .github/workflows/CLAUDE.md is documented as having 'no exceptions'. Validate toolkit_ref does no network egress (case/printf/grep), so the threat model is unchanged either way — but conventionally bullfrog comes first. Validate still runs before any step that consumes toolkit_ref to download action.yml or post-*.ts content, so its security role is preserved. Also extends the claude-docs-check.yml comment block to note that external repos copying the consumer workflow as a template should keep toolkit_ref pinned to 'main' — the reusable worker's allowlist blocks fork branches but still permits 'next'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): tighten bullfrog allowlists per workflow from empirical egress data
Three workflows tightened based on bullfrog log analysis. Each workflow
now has an allowlist that matches its actual observed egress instead
of inheriting the shared 9-entry kitchen-sink list.
Method: pulled all available successful run logs per workflow over the
last 30 days. Extracted bullfrog audit-mode and block-mode events.
Cross-referenced with each workflow's runtime code (Linear MCP yes/no,
OAuth yes/no, package manager). Settled on the smallest allowlist that
covers observed setup-phase egress plus required runtime hosts that
audit mode can't see (DNS-reply coalescing per agent/queue_audit.nft).
Cross-cutting drops vs the previous shared 9-entry allowlist:
github.com / *.github.com — in bullfrog defaultDomains
(agent/agent.go:18); 0 audit lines
across all 17 audit-mode runs sampled.
claude.ai / *.claude.ai — OAuth path; all three workflows use
ANTHROPIC_API_KEY path; 0 audit lines.
bun.sh — setup-bun fetches from github releases
via *.githubusercontent.com; verified
by 15 _generate-pr-metadata setup-bun
runs showing 0 bun.sh lines.
*.githubusercontent.com — only release-assets.githubusercontent.com
is empirically used; pinned to the
specific subdomain.
*.npmjs.org — only registry.npmjs.org is used;
pinned to the specific subdomain.
Per-workflow tuning beyond cross-cutting drops:
_claude-code-review.yml — block mode (unchanged). Empirical: 2
runs showed only Datadog logs agent
blocked. No Linear MCP, dropped
api.linear.app. Final allowlist: 3
entries.
_claude-docs-check.yml — block mode (unchanged). Empirical:
12 runs showed only Datadog blocked.
No Linear MCP. Final: 3 entries.
_generate-pr-metadata.yml — FLIPPED audit → block. Empirical: 15
audit runs showed registry.npmjs.org
+ release-assets.githubusercontent.com.
No Linear MCP. Final: 3 entries.
NOT TOUCHED (insufficient data):
_claude-main.yml — 0 runs in 30d; can't validate
empirically. Leaving the existing
allowlist as-is until data exists.
_update-action-versions-worker.yml — 0 runs in 30d (weekly cron
fired outside the window).
Wildcards: bullfrog uses Go's path.Match (agent/agent.go:181), so
*.foo.com covers any subdomain depth. We pin to exact subdomains
empirically observed instead, matching the "based only on logs"
review directive.
Sister PRs:
Uniswap/default-token-list#2485 — dtl Task Worker (per-job tight)
#514 — aitk Task Worker (per-job tight)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): allow raw.githubusercontent.com for plugin discovery
build-plugin-config/action.yml fetches marketplace.json from
raw.githubusercontent.com/Uniswap/ai-toolkit/next/.claude-plugin/marketplace.json
for plugin discovery. The audit-mode sampling that informed the tight
allowlists missed this domain because bullfrog v0.8.4 queues DNS replies
(agent/queue_audit.nft) and once a hostname is resolved+cached the kernel
resolver short-circuits — no further audit events fire. In block mode the
fetch now fails and breaks the "Build plugin configuration" composite
step in docs-check, code-review, and generate-pr-metadata.
Add raw.githubusercontent.com to all three tightened allowlists and update
the explanatory comments so future maintainers understand the audit-mode
caveat that led to the gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): allow claude.ai for Claude SDK install + runtime
Second iteration of audit-mode-coalescing miss: the audit logs that
informed the original tightening showed no claude.ai traffic, so the
allowlist dropped it. Block-mode CI then failed at the Claude SDK
invocation with:
##[warning] Blocked DNS request to claude.ai from unknown process
ReferenceError: Claude Code native binary not found at
/home/runner/.local/bin/claude
The Claude SDK action installs the native CLI from claude.ai/install.sh
and also touches claude.ai at runtime for telemetry/session-token
lookups even on ANTHROPIC_API_KEY auth — none of which appeared in
audit logs because bullfrog v0.8.4 queues DNS *replies* and cached
resolutions emit no further events (agent/queue_audit.nft:13-14).
Add claude.ai to all three tightened allowlists (_claude-docs-check.yml,
_claude-code-review.yml, _generate-pr-metadata.yml). Update inline
comments to record the methodology lesson so future tightenings don't
repeat the trap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): allow downloads.claude.ai for Claude CLI binary install
Third iteration of audit-mode-coalescing miss. After adding claude.ai
the Claude SDK install script entry point resolved, but its actual
binary download fetches from a different host:
curl: (6) Could not resolve host: downloads.claude.ai
ReferenceError: Claude Code native binary not found at
/home/runner/.local/bin/claude
Add downloads.claude.ai to all three tightened allowlists.
Lesson recorded in inline comments: bullfrog v0.8.4 audit-mode coalescing
hides multiple domains per workflow, so the correct iterative tightening
methodology is to flip block-mode in a single test run and harvest from
the *block* logs (which DO show every blocked destination), not the
audit logs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): drop 'Dropped:'/'Pinned:' enumerations from allowlist comments
git blame on allowed-domains: already reveals what was removed and why,
and runtime block-mode CI failure logs show what's currently blocked.
Keep the rationale for each *kept* domain inline; let history speak for
the rest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…523) * fix(slack-oauth-backend): use fresh exchange bot token, drop static SLACK_BOT_TOKEN The users.info enrichment and DM delivery both authenticated with the static SLACK_BOT_TOKEN env var. With token rotation enabled, bot tokens are short-lived (xoxe, ~12h) and a static env var goes stale; it also dies on every reinstall. This surfaced as `account_inactive` from users.info in the OAuth callback (observed in Vercel logs). Both consumers run during the callback, when the fresh bot token from the exchange (tokenResponse.access_token) is in hand. Use it directly: - handler.ts: enrich users.info with the fresh token; surface it as OAuthResult.botAccessToken. - routes/oauth.ts: pass botAccessToken to createSlackClient for DM auth. - client.ts: SlackClient takes a per-request botToken instead of reading config; createSlackClient bypasses the singleton when a botToken is given so a per-request token is never cached across installs. - config/index.ts: remove slackBotToken; .env examples + README + DEPLOYMENT docs updated to note there is no static bot token. Health check (/health testAuth) gracefully reports token_rotation mode when no bot token is present, unchanged. Specs updated: bot token now passed explicitly to the constructor in client tests; new tests cover the oauth-only constructor path and the singleton-bypass factory behavior. 74/74 pass, typecheck + lint clean. * test(slack-oauth-backend): distinct bot/user token fixtures prove sourcing Addresses code-review feedback on #523: the handleCallback fixture used the same value for the bot slot (access_token) and user slot (authed_user.access_token), so the botAccessToken assertion passed vacuously and couldn't catch a regression that sourced it from the wrong slot. Give the bot slot a distinct xoxb- value. This also surfaced (and the test now pins) the bot-token fallback: when no user token is present, the selected accessToken correctly falls back to tokenResponse.access_token.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
||
| # Disable Bun's automatic bunfig.toml loading from CWD. When a workflow | ||
| # checks out PR head content (this workflow checks out PR refs to read | ||
| # diffs and post comments), `bun run` would otherwise auto-load |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
Reusable workflow (on: workflow_call) uses bun run. Bun auto-loads bunfig.toml from CWD, and its preload array executes arbitrary code before the intended script. If the workflow checks out fork PR code, this enables RCE with access to the caller's secrets. Fix: set BUN_CONFIG_FILE=/dev/null in the job env.
To resolve this comment:
✨ Commit fix suggestion
| # diffs and post comments), `bun run` would otherwise auto-load | |
| BUN_CONFIG_FILE=/dev/null bun run "$POST_SCRIPT" $SCRIPT_ARGS |
View step-by-step instructions
- Update the line that runs the Bun script to set the environment variable
BUN_CONFIG_FILEto/dev/nullto prevent Bun from auto-loading an attacker-controlled configuration file. - Change the execution line to:
BUN_CONFIG_FILE=/dev/null bun run "$POST_SCRIPT" $SCRIPT_ARGS.
This protects the workflow from remote code execution via malicious bunfig.toml files in forked pull requests.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by bun-run-in-reusable-workflow.
You can view more details about this finding in the Semgrep AppSec Platform.
📚 Documentation Check ✅Verdict: Passed No plugin files were modified (no version bump required). README.md, DEPLOYMENT.md, and env example files were already updated in the PR. One documentation inaccuracy exists in .github/workflows/CLAUDE.md but fail_on_missing_docs is false. PR #528 Documentation Check — PASSSummary of ChangesThis PR has two main areas of change:
Documentation Status
Key Finding
Missing Updates
Suggestions (3)
🤖 Generated by Claude Documentation Validator | Mode: |
|
Closing in favor of new release branch: release/next-to-main-20260608-175845 |
Production Deployment
This PR promotes the
nextbranch tomainfor production release.Commits included (12):
Merge Strategy
Using merge commit to preserve full commit history for changelog generation.
Temporary Branch
This PR is created from a temporary branch
release/next-to-main-20260601-185159that will be deleted after merge.This PR was automatically created by the Update Production workflow.
Changes Summary
✨ Features (1)
🐛 Bug Fixes (6)
🔧 Maintenance (4)
Full Commit List