Skip to content

Add a CI check that keeps dashboards' browser code off the Public API - #118

Closed
anth-volk wants to merge 3 commits into
dashboard-api-skillfrom
public-api-access-check
Closed

anth-volk wants to merge 3 commits into
dashboard-api-skillfrom
public-api-access-check

Conversation

@anth-volk

Copy link
Copy Markdown
Contributor

Fixes #115

Stacked on #116 (policyengine-api-access), whose skill gains the check's section. Its base is that PR's branch. It does not need #PR_2, and the two merge together without conflicts.

Summary

skills/policyengine-api-access/scripts/check_public_api_access.py fails a dashboard's pull request when browser code calls the Public API (api.policyengine.org) directly. It reads files only, uses only Python's standard library, and follows the cost checker: a pinned copy downloaded in CI, the same command line and output, and declared exceptions.

Rule Severity Reports
PA1 error Browser code that names api.policyengine.org or one of the Public API's Cloud Run addresses (policyengine-api[-staging]-<hash>-<region>.a.run.app, tagged revisions, policyengine-api[-staging]-<number>.<region>.run.app), also when the host is joined from string parts, an array join, or a template with 'policyengine.org' or 'api' in the same file
PA2 error A NEXT_PUBLIC_*, VITE_*, REACT_APP_*, GATSBY_* or EXPO_PUBLIC_* variable named for a Public API address or credential, or set to the Public API's address, its server Auth0 tenant, a token or a server credential; or next.config or Vite config that reads a client secret
PA3 warning vercel.json, netlify.toml, _redirects or build config that names the Public API
PA4 warning Browser code that builds /{country}/economy/{policy}/over/{baseline} or /{country}/calculate-full

Browser code is every JavaScript, TypeScript, Vue, Svelte, Astro and HTML file except server code: top-level backend/, server/, scripts/, functions/ and similar; api/ at the root of a project that is not Next.js; Next.js route handlers, pages/api/, pages/_document, middleware and instrumentation; *.server.*; modules starting with "use server" or importing server-only. In a Next.js project an import graph decides: a module that a "use client" module or a Pages Router page reaches is browser code; one that only server components reach is server code; one nothing imports counts as browser code. Python, tests, stories, build output and comment-only lines are skipped.

Where it lives, and how dashboards run it

In this repository, next to the skill, the same way as policyengine-cloud-costs/scripts/check_deploy_config.py: the scaffold and /new-tool already download that checker from a pinned commit (CHECKER_COMMIT) and run it with the runner's python3. A composite action or a package would add a release process for one file. create-dashboard.yml builds dashboards with the plugin's scaffold agents, so new dashboards get the step through the scaffold.

  • templates/public-api-checks.yml: a workflow that runs the pinned script on pull requests, with contents: read.
  • The skill's new "Check it in CI" section: the steps, the rules, what counts as browser code, declarations, and what it cannot see (values set only in Vercel or GitHub settings, encoded strings).
  • targets/claude/agents/dashboard/dashboard-scaffold.md and targets/claude/commands/new-tool.md: every new dashboard's CI gets the step, and /new-tool's local checks run it. Adding it to existing dashboards is part of moving each one onto the dashboard API.

Environment variables and secrets

The template and the scaffold steps add a workflow step to dashboards; this repository's own workflows are unchanged.

Name Kind Provider Wiring Verified
CHECKER_COMMIT step env A literal commit SHA written into the dashboard's workflow when the step is copied (CHECKER_COMMIT_SHA placeholder) Step env: → the curl URL in the same step Yes, by tests/test_public_api_access_checker.py::test_workflow_template_runs_a_pinned_copy_with_read_only_permissions
GITHUB_TOKEN — Not used: raw.githubusercontent.com serves this public repository without one — —

No secret is read. Not verified: a run of the template on GitHub Actions; the script ran locally only.

Tests

tests/test_public_api_access_checker.py writes small repositories into a temporary directory and scans them: each PA1 form in a Vite app; Next.js modules reached from a client component, shared with a server component, or imported by nothing; Pages Router pages; HTML, Vue, Svelte and static sites; 22 server, test, comment, build-output and other-host cases that must not be reported; Next.js server code; an api/ directory in a Next.js project; 16 PA2 cases and 10 that must not be reported; PA3 and PA4; declarations with and without a reason; exit status 0, 1 (also with --strict on warnings) and 2; standard-library imports only; the workflow template; the skill's section; and the scaffold steps. Two regression tests came from running it on dashboards: tsconfig.json and package.json with URLs in strings, and a file that starts with hundreds of comments (an earlier pattern took minutes on policyengine-model).

  • Red, e545f44: 87 failed on their assertions; exit code 1. The two regression tests failed against the first version of the script (one by timeout) before its fix in the green commit.
  • Green, fe46e86: 89 passed; exit code 0.
  • CI: RED_RUN / GREEN_RUN.

On shallow clones of 18 dashboards' default branches it reported every browser caller among them that the Public API caller inventory lists, at the calling file (aspen-eitc-ctc, autumn-budget-2025-in-review, ccdf-calculator, cliff-watch, keep-your-pay-act, oregon-kicker-refund, policyengine-model, refundable-credit-conversion, state-legislative-tracker, us-state-eitcs-ctcs), and nothing in child-poverty-impact-dashboard and marriage (whose backends call the Public API) or in the six that don't call it. autumn-budget-2025-in-review also reports two lines of UI text that name the address. Each scan took under a second.

🤖 Generated with Claude Code

anth-volk and others added 3 commits November 10, 2026 22:17
Adds tests/test_public_api_access_checker.py for
skills/policyengine-api-access/scripts/check_public_api_access.py, which
does not exist yet. Each test writes a small dashboard repository and
scans it:

- PA1 (error): browser code names api.policyengine.org or one of the
  Public API's Cloud Run addresses, including hosts built from string
  parts, in Vite apps, static sites, Pages Router pages and Next.js modules
  that a client component imports;
- server code is not reported: Python backends, scripts, Vercel
  functions, Next.js route handlers, server components, server actions,
  server-only modules and middleware, plus tests, comments, build output
  and the household API and v2 hosts;
- PA2 (error): a NEXT_PUBLIC_, VITE_ or REACT_APP_ variable holds a Public
  API address, the Public API's Auth0 tenant, a token or a client secret,
  or build config copies a server credential into the bundle; ordinary
  public variables and server secrets are not reported;
- PA3 and PA4 (warnings): deploy config that proxies to the Public API, and
  browser code that builds a Public API path;
- declared exceptions, the command line's exit status, standard-library
  imports only, the pinned workflow template, the skill's CI section, and
  the CI step in new dashboards' scaffolds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
skills/policyengine-api-access/scripts/check_public_api_access.py reads a
dashboard's repository and fails when browser code calls the Public API:

- PA1 (error): browser code names api.policyengine.org or one of the
  Public API's Cloud Run addresses, also when the host is joined from
  string parts;
- PA2 (error): a NEXT_PUBLIC_, VITE_, REACT_APP_, GATSBY_ or EXPO_PUBLIC_
  variable is named for, or set to, a Public API address, its Auth0
  tenant, a token or a client secret, or next.config or Vite config reads
  a client secret;
- PA3 and PA4 (warnings): deploy or build config that names the Public
  API, and browser code that builds a path only the Public API serves.

Server code is not reported: top-level backend, server and scripts
directories, Vercel functions, Next.js route handlers, middleware,
server actions and server-only modules, and Next.js modules that only
server components import (an import graph from "use client" modules and
Pages Router pages decides). It uses only the standard library and runs
in linear time; it follows the cost checker's command line, output and
public-api-ok declarations.

templates/public-api-checks.yml runs a pinned copy with read-only
permissions. The skill's new "Check it in CI" section says how to add it,
and the dashboard scaffold and /new-tool add the step to every new
dashboard's CI. Existing dashboards adopt it in their own migrations.

On 18 PolicyEngine dashboards it reports every browser caller listed in
the Public API caller inventory, at the file that calls it, and nothing in
the two dashboards whose backends call the Public API or in those that
don't call it. Two regression tests cover what that run found: config
files with URLs in strings, and a file that starts with many comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s-check

Brings in the rebuilt dashboard manifest, which the when-to-use guide test
reads, and the skill's description of the firewall's 429.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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