Skip to content

About

Palomar submission server: a Cloudflare Worker over GitHub state

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Palomar submission server

A Cloudflare Worker that takes submissions, establishes either that whoever submitted can push to the repository or that an active Technical Maintainer is running a non-registerable workflow test, and keeps every durable fact in GitHub.

Live at https://submit.palomar-registry.org.

Why it holds no state

The Worker keeps nothing between requests. A submission is a directory of JSON files committed to the private PalomarRegistry/PalomarSubmissionState repository, and every transition is a commit. Switch the Worker off and the record is complete; the operator CLI can drive any submission to a terminal state without it. That is the property that stops this becoming something we cannot leave.

What is secret, and what is not

Stated on the intake form, in these words, because it is not what people assume:

  • Public from the moment you submit: the repository, commit, and selected Comparator configuration path. Verification runs in a public Actions workflow whose logs are public.
  • Never public unless the author registers: the review, its findings, and the submitter's identity.
  • "Private" means not public, not confidential. Reviews are readable by operators, GitHub, and the model provider, and are kept indefinitely.

Identity

There is no account. There are two ways in, and a record says which one it came through, because they do not prove the same thing.

A person signs in with GitHub once, so the server can check permissions.push on the repository being submitted and read the login it is answering for. The token is discarded immediately and written nowhere; the login is kept, because it is what the quotas count against. GitHub answered for the same account that authorised Palomar, so one account both can push and identified itself.

An agent has no browser, and drives a tag at the submitted commit plus a secret gist carrying the same challenge. Creating a ref needs the same write access; the gist supplies an identity, because a ref records no author and a third party cannot ask GitHub who has push. That establishes that someone who can push submitted the repository and that an account named itself, which are not provably the same account, so the record carries separately-attested rather than same-account and the two are not treated as equivalent anywhere.

A Registry-correction agent uses a narrower proof for a different authority. Corrections do not require write access to the source repository, which they cannot move. The agent creates only the secret challenge gist; GitHub's numeric id for its owner must be in src/technical-maintainers.js. The durable proof is the same technical-team-correction / active-technical-team-membership authority recorded by the dashboard path. A source-repository tag would prove an irrelevant capability and is deliberately neither requested nor read.

An access token is the ordinary way back to a submission. On the agent path there is no link to put it in, so /api/verify returns it in the response body. On the browser path it is carried after the #, because a browser leaves that part out of the requests it makes, so it does not reach an access log or a Referer header. The status page presents its fragment as an Authorization: Bearer header on each private request and explicitly omits cookies. The fragment belongs to the page rather than the browser profile, so two submissions open at once cannot replace one another's credential. Saying it is "never sent to a server" would still be wrong, and the page does not say so: it says to treat the link like a password, which is the part a submitter can act on.

Losing the link is not permanent while a submission remains in the open-work queue. /submissions, also linked from the submission form, starts a separate recovery sign-in. GitHub identifies the same numeric account id recorded in the original proof; Palomar then shows every matching open submission and rotates one recovery token for each. The original token remains valid. Numeric identity, not a reusable login, is the authority: GitHub logins can be renamed and later reused.

After a successful browser OAuth callback, Palomar also sets a signed, host-only identity cookie for twelve hours. It contains the account's numeric id, login, and expiry, but no GitHub access token; the OAuth token is still used only during the callback and discarded. The submission form uses that local identity to list matching open-work metadata automatically. Listing is read-only. Choosing Open this submission rotates the recovery capability for only that submission, so merely loading the form neither writes State nor invalidates a previously recovered link. An absent or expired identity cookie falls back to the ordinary /submissions OAuth flow.

For a completed verification failure, the browser reads the public run's job and check annotations directly from api.github.com so it can show the actual error beside the run link. Those bounded requests carry neither a GitHub credential nor the Palomar cookie or fragment, and the site's no-referrer policy keeps the private status URL out of them.

The browser status page previously used a single __Host- cookie. Although it was protected from sibling hosts, every tab on this host shared it; opening a second submission therefore changed what the first tab read. Current status pages use only their own fragment capability for private reads and actions. The server still honours /session cookies for legacy clients, but current pages neither create nor send them.

The short-lived cookie that binds a browser OAuth callback to the tab that started it has the same host-only properties and a different name for every pending intake. Only the callback consumes the exact nonce-derived name, and an ambiguous or case-confusable protected name, or a malformed binding, is refused before it can read or consume pending State.

Push access is not authorship. It does not establish approval from the responsible authors of a substantive formalization, and does not replace the declaration a submitter makes about that.

Browser sign-in requests no GitHub OAuth scopes: public identity and public repository information are sufficient. Technical Maintainer authority comes from the reviewed numeric GitHub ids in src/technical-maintainers.js, not from organization visibility delegated by every submitter. Allowlisted Technical Maintainers bypass the ordinary per-principal start interval and the owner and submitter in-flight caps for every submission, independently of the authorization relationship selected on the form. The durable OAuth proof records that authority.

When an allowlisted maintainer submits a public repository and pinned commit without write access, the browser path automatically records it as a technical test. The explicit technical-test relationship reaches the same path. Its durable record says test_submission, does not claim push_verified, and carries a distinct Technical Maintainer proof. Technical tests remain browser-only; the agent exception is limited to a structurally valid Registry correction and checks the gist owner's numeric id before admission. The pre-authentication address throttle still applies because the account is not known until the OAuth callback or gist verification completes.

Private operational dashboard

Technical Maintainers enter at https://submit.palomar-registry.org/dashboard. The dashboard includes the protected exceptional Registry-correction editor; the operational procedure is the PalomarPolicy maintainer-corrections.md runbook. The ordinary submission form does not expose this editor.

/dashboard shows aggregate submission throughput, review-round counts, landing distributions, and bounded model-spend distributions. It never reads a review or serves a submission id, repository, Comparator path, commit, or submitter identity. The State reporting workflow derives reports/dashboard.json in a daily or manually dispatched full sweep; the page reads that one private file and identifies the exact immutable State submissions/ tree and latest event included, so lag is visible rather than disguised as current data.

Dashboard sign-in also requests no GitHub OAuth scopes. The callback requires a numeric id in the same checked-in Technical Maintainer allowlist, discards the GitHub token immediately, and issues a signed host-only session lasting fifteen minutes. Removing a maintainer takes effect no later than that expiry after the allowlist change is deployed. The signature is domain-separated from submission-token digests while reusing the existing TOKEN_PEPPER; there is no additional secret or durable login store. The OAuth state and session cookie both reject duplicates and are never cached. Dashboard OAuth initiation and callback share the intake address limiter, so an unauthenticated loop cannot turn into unbounded GitHub token exchanges. Every dynamic OAuth response carries the same no-referrer and security headers as the rest of the Server. The API additionally requires a same-origin browser request; the OAuth landing page itself remains a top-level cross-site navigation.

The machine-readable aggregate is available at /api/dashboard under the same session. The Server validates that the stored document has the identity-free dashboard contract with an exact field-name allowlist and deep value shapes, instead of trying to spot a few forbidden identity fields. The contract is represented by versioned aggregate fixtures retained as backward-compatibility examples, not as evidence about current producer output. Pull-request CI runs the current consumer against every dashboard fixture from the base commit, so editing a fixture cannot disguise a backward-incompatible change. State CI runs its real producer output through this repository's consumer, while Server CI proves that a candidate still accepts every schema version retained by its base commit. Together those two directions prevent either repository from crossing the contract boundary alone, without granting the public Server repository access to private State. The page and API link directly to the private Database's one-person Moderator forms for takedown and restoration. Those links are operator conveniences, not State data.

Dashboard contracts are append-only by schema_version. A new required field, removed field, changed literal, or narrowed value shape requires a new schema version. Deploy the Server consumer accepting both versions first; only then change State to emit the new version. Keep the earlier consumer until no stored report uses it. Corrections that make a producer conform to its already-declared schema do not create a new version.

Configuration

Variables live in wrangler.jsonc. Secrets are set with wrangler secret put and never appear in the repository:

Secret What it is Reach
OAUTH_CLIENT_ID GitHub OAuth App client id, for public identity and submission push-access checks —
OAUTH_CLIENT_SECRET its client secret —
GITHUB_TOKEN reads and atomically advances submission State, asks the reviewer to run, and reads public repository metadata for the repository being submitted PalomarSubmissionState, contents and actions, plus public reads
SUBMISSION_TOKEN starts and reads verification runs, and reads the submitter's public ref and gist while checking a proof PalomarSubmission, actions, plus public reads
TOKEN_PEPPER so a leaked state repository does not yield live links, and to authenticate short-lived GitHub identity cookies under a domain-separated HMAC key —

The checked-in Technical Maintainer ids are the source of truth. Adding or removing authority is an ordinary reviewed change to src/technical-maintainers.js. GitHub login renames require no update because the numeric id is stable; a deleted account can no longer authenticate, so no scheduled synchronization job or organization-reading credential is needed.

The State token's existing repository Contents: write grant covers the Git tree, commit, and non-forced reference update used for atomic admission; it does not need repository-administration permission.

Two GitHub tokens, not one, because a fine-grained token grants the same permissions to every repository it names. A single token covering both repositories would carry write access to the verification code itself, and this server is internet-facing and takes untrusted input: a leaked secret would become a way to forge mechanical verification. Neither token needs admin anywhere, and neither can touch PalomarDatabase.

The verification run

Dispatching a workflow does not return a run id, so the server finds the run it started by name: the workflow's run-name is Verify submission <id>. The submission id is public, so anyone able to dispatch that workflow can produce a run carrying it. The name is therefore matched exactly, and the run is pinned the first time it is seen and never replaced. The reviewer accepts only the pinned run id, so the name is not the trust boundary in either place.

Consent

Registration is the submitter's decision and nobody else's. /register records consent together with the digest of the review the submitter was shown, and the reviewer refuses to register anything whose bytes differ, so a revised review needs fresh consent rather than inheriting the old. Withdrawing before consent leaves no public trace of the review or its findings. After consent, the reviewer still refuses to merge a withdrawn record, but source-preservation or rendering work that already started may remain public. Registration requires a current review that identified no blocking problem; an older in-flight review must be rerun. A technical-team test can reach the same review screen, but its registration control is disabled with an explanation. /register and the reviewer independently refuse it, and State validation reports any test record edited by hand to carry consent or registration state. Test submissions also cannot request an automated metadata-repair pull request.

Withdrawing also empties the record, in the same write that closes it. What the submission was and what happened to it stay; what identified the person who sent it does not. The submitter's login goes, and so do both of the free-text fields the submitter wrote: the notes, and whatever was typed to evidence an authorization relationship. Either of those can name people who never submitted anything, which is why neither survives the submission they were written for. A second event on the record's own timeline says that they went. Replacing an earlier submission of the same repository closes it the same way and scrubs it the same way.

The numeric GitHub principal is the exception, and it has to be. Recovery intersects index/principals/<digest>.json with the reviewer's queue and then verifies the numeric principal stored in every record it selected, before it filters out the closed ones; a withdrawn id stays queued until the reviewer's next pass drops it. A record with no principal would therefore not read as "closed, ignore it" but as a locator naming somebody else's submission, which fails closed and would take the whole recovery page down for as long as the id is queued. Replacement checks the same field before refusing to replace something already closed.

Retaining it is a deliberate trade rather than a claim that what remains is anonymous. A GitHub account id is stable and publicly resolvable: the provider will hand back the account for an id through an endpoint that needs no permissions at all, so anyone who can read the state repository can still get from a withdrawn record to the account that sent it. What the pepper buys is that listing index/principals/ or index/rate/ enumerates nobody, because those file names are digests rather than ids; it does not make the id inside the record anonymous. The scrub removes the direct login and both free-text fields, and keeps the one bare identifier the integrity checks above fail closed without.

It is not erasure. The state repository's git history retains the record as it was before the withdrawal, and what happens to that is a question for the state repository's retention policy. Scrubbing the current tree is what stops every later read, sweep, clone, and index rebuild carrying it forward.

The state a submission holds

submissions/<id>/state.json   # the record: status, source, authorization, run, consent
submissions/<id>/review.json  # the private review, once delivered
index/tokens/<digest>.json    # original or recovery token digest to submission id
index/principals/<digest>.json # private submission locator for one submitter
index/rate/<digest>.json       # how long this submitter waits before starting
                              #   again; the digest is the only thing naming
                              #   them, and the body names nobody
index/inflight.json           # admission slots, released by cron reconciliation
index/open.json               # the reviewer's queue: added here, pruned there
index/review-timing.json      # how long recent reviews took, for the estimate
pending/<digest>.json         # a one-time intake nonce; an OAuth submission may
                              #   retain its proved identity until the choice,
                              #   and a record older than fifteen minutes is
                              #   refused on read and swept on the next pass

index/inflight.json has exactly one top-level field, open, and no versioned pre-launch variants. Each entry has exactly id, owner, submitter, and at. The id is the current 12-character lowercase submission id; owner and submitter are GitHub logins (owner may be null); and at is a UTC timestamp at whole-second precision. Duplicate ids, missing or extra fields, noncanonical timestamps, and a missing file stop admission and reconciliation visibly. They are not coerced to an empty list or treated as if the repository owner were the submitter.

The index enforces at most two active verifications for one repository owner and at most one for one submitter. It deliberately has no global admission cap: existing work by unrelated people cannot make intake refuse everyone. The edge address throttle and the submitter's exponentially increasing start interval remain independent controls.

index/open.json is likewise required. The server consumes only its schema_version: 1 marker and an open array of unique current submission ids. Every other top-level field belongs to the reviewer: the server preserves it on append without interpreting its shape or timestamp precision. A missing or malformed queue is never replaced as though it were empty.

Recovery first reads the authenticated principal's pepper-keyed locator, then intersects its submission ids with the current reviewer queue. It reads only that person's current records rather than fanning out across the shared queue, checks every stored numeric principal id, and atomically rotates their optional recovery_token_sha256 pointers. Repeated recovery therefore costs current work rather than registry history and retains at most one recovery pointer per submission. It neither invalidates the original pointer nor makes a login name an authority.

Each locator contains exactly schema_version: 1 and a unique submissions array. Admission appends to it atomically with the new submission; terminal ids may remain because recovery intersects it with index/open.json before reading records. Submissions from an OAuth-verified Technical Maintainer use the same locator without acquiring the ordinary submitter backoff or in-flight caps.

The pure intake normalization and validation live in src/intake-contract.js; admission caps and rate-record projection live in src/admission-contract.js; the checks for these indexes, the current review marker, and the submitter-visible review projection live in src/state-contract.js; and exact request credential transport and origin classification live in src/request-credentials.js. These four modules do not perform I/O. Validated shared-index reads, reviewer-queue appends, admission-slot release and reconciliation writes, verification reconciliation, and abandoned-intake cleanup live in src/submission-lifecycle.js. Durable cooldown-reset work and atomic resets live in src/rate-reset.js. The Worker in src/index.js remains the composition root: it decides which routes require credentials and owns body decoding, responses, authorization, request-path record and rate reads and optimistic writes, the admission-slot append, and route-level ordering.

Admission uses GitHub's Git Data API as a transaction boundary. The Worker reads the pending proof, rate record, capacity index, reviewer queue, and the two create-only paths at one exact main commit. One derived tree then consumes the proof and writes the submission record, capacity reservation, reviewer queue entry, token index, rate update, and pending cooldown-reset entry. A non-forced ref update publishes all of them together. If another writer moved main, the update is rejected and the complete decision is recomputed from the new head; unreachable losing tree and commit objects never become State. Policy refusals publish only the proof deletion. Damaged or unavailable State publishes nothing and leaves an agent's proof retryable within its reported attempt budget. If a ref-update response and the follow-up reachability checks are all unavailable, the Worker reports the outcome as unknown instead of claiming that the proof survived.

A browser submission pauses after OAuth when that submitter already has open work. The choice page includes fresh links to all of it. When a matching repository is selected for replacement, withdrawing the earlier record, exchanging any admission slot, consuming the pending OAuth proof, admitting the new record, and updating both queues happen in the same transaction. A rate or capacity refusal writes none of those changes, so “start the new one” cannot strand the earlier submission halfway through the choice.

The external verification dispatch cannot be part of a Git commit. A newly committed verifying record therefore doubles as a durable outbox item and carries a short dispatch lease. The admitting request tries immediately. The ten-minute lifecycle first searches for the workflow run (covering a crash after GitHub accepted an ambiguous dispatch), then one reconciler claims an expired lease and retries. It does not release capacity or declare the dispatch lost merely because a credential or provider outage needs repair. After three complete searches and dispatch attempts, an undiscoverable run is irrecoverable: the scheduled pass records the Palomar-owned dispatch-lost status before releasing its slot. A pinned run that GitHub confirms is gone is handled the same way and is never replaced by a namesake dispatch. Queued or running work remains reserved without an age timeout.

The full workflow performs authoritative preparation before installing its verification-only toolchain. A failed run first enters a reporting status; the Reviewer ingests the exact run's artifact and atomically records bounded, owner-labelled diagnostics before the status page treats the result as terminal.

Before pointing a fresh or staging Worker at a new state repository, copy the three files in state-bootstrap/index/ to index/ and commit them. Deploying the Worker before that initialization deliberately leaves intake unavailable; it does not silently disable the owner and submitter limits.

Admission and cooldown resets use the multi-file transactions described here. Other lifecycle and submitter decisions remain ordered Git commits rather than one global transaction: their operations are idempotent or deliberately leave an earlier durable state that the next request or scheduled pass can finish. Git State continues to be the auditable source of truth; no separate coordinator or export process is required.

index/open.json holds every submission the reviewer is not yet finished with. This server adds an id when it admits one, and the reviewer drops one when the record says there is nothing left to do to it, so a reviewer pass costs the queue rather than the size of the registry. The reviewer can rebuild this derived queue from the records on its maintenance path, but the server does not silently reconstruct or overwrite it. A missing or malformed queue makes intake and affected status transitions unavailable until the reviewer or an operator restores it.

index/repairs.json is a separate durable outbox for submitter-authorized metadata repairs. A successful current-policy preliminary check first renders the exact project-wide project.description that registration would use as the public abstract, with the selected Comparator configuration and its declarations shown separately. It is read-only until the submitter chooses Edit description. That choice opens the same authenticated pull-request path, restricted to the one description field. When preliminary checks instead find only field-level formalization.yaml problems, the intake page renders the complete guided editor before authentication. Its bounded edit payload is stored in the private pending intake. After OAuth proves write access, the Worker re-reads the file at the exact commit, repeats the portable metadata validation, checks either that the edits cover every current field problem or that the voluntary request changes only project.description, and verifies that they produce valid portable metadata. It then atomically creates a changes-required submission and queued repair. It dispatches the repair worker instead of the full verification workflow; that worker still runs authoritative preparation against the generated commit before opening a pull request.

The later POST /api/repair path requires the submission capability, the digest of the current failure, a changes-required record, and fields explicitly marked repairable in that failure. It atomically writes the request, its queue entry, and the record marker. Profile 3 covers every mechanically required metadata field, including the multiline project description and structured people, source, and automation lists, and requires one complete submitter-confirmed payload before it queues a pull request. Safe values from an older metadata shape may be shown as editable prefills, but classifications, maintainers, source relationships, repository roles, and review claims are never inferred. Malformed YAML, non-mapping roots, aliases, unsafe paths, and mixed unsupported failures remain manual-only.

index/rate/<digest>.json slows down a submitter who keeps starting and never finishes. Starting work is the expensive act: it dispatches either verification or an authenticated repair, both of which consume somebody's runners whether or not anything comes of them. So the interval is sixty seconds to begin with, doubles every time work is started, and is put back to that floor only by a completed registration or by one of the first two preparation failures that asks for repository changes. Later metadata failures keep the accumulated interval, so the correction concession cannot turn repeated preparation work into an unbounded cheap loop. A submission that fails full verification, or is withdrawn, leaves it where it is, because those are exactly the loops worth slowing down; ordinary metadata correction is not treated as abuse.

The Server owns the rate document's current schema_version: 1 contract. A present document records a positive integer starts, an integer interval_seconds of at least sixty, and canonical UTC-seconds last_start_at and next_allowed_at timestamps. It carries nothing that identifies the submitter: the file name is a peppered digest so that listing the directory enumerates nobody, and a cleartext login in the body handed that back to anyone who could read the files.

That is enforced rather than merely intended. Like the dashboard aggregate and the principal locator, this contract is an exact field-name allowlist, so a future writer cannot quietly reacquire an identity field: submitter_login would have to be added here, past these tests, before it could reach a file. Two fields the Server no longer writes are on the list because older documents carry them, and refusing those would lock their submitters out of intake rather than merely losing information: login, and submission_ids from before the principal locator took that job over. Between them and the current five, that is every field any version of the Server has ever written to a rate document, so the allowlist costs nothing in compatibility. Both are accepted on read, neither survives a projection, and a registration reset names its output field by field rather than spreading the document it read, which is how those two outlived the writers that produced them. A login that is present but is not a GitHub login is a malformed document and still fails closed.

The cost of the allowlist is that a deployment which adds a field cannot be rolled back past one that does not know it without refusing the documents it wrote. That is the same append-only discipline the dashboard contracts state above, and the reason to add a field by bumping schema_version rather than by widening this quietly.

The State repository's whole-tree validator deliberately treats these producer-owned fields as opaque, so the Server validates all of them before admission and before projecting a reset. A missing file is a first start; a malformed present file makes intake temporarily unavailable rather than silently granting the floor. A malformed file also leaves a registration reset unapplied until repair, but does not hide the already-registered result from its submitter.

There is no ceiling, and that is deliberate rather than an omission to fix. Twenty starts with nothing registered is already years; nobody submitting in good faith reaches it, and the failure worth designing for is the other one, a person locked out with no way back on their own. The escape hatch is an operator deleting one file, which is why the file records the time beside the interval. Which file that is comes from the pepper, not from the file: tools/rate-path.js turns a login into the path. The name is a digest for the same reason index/tokens/ is, and the body is anonymous for the same reason again, since a digest for a name buys nothing if the file it names says the name.

The server applies resets without sharing TOKEN_PEPPER with reviewer CI. Admission atomically appends each rate-limited submission to the server-owned index/rate-resets.json queue (schema_version: 1, open: unique submission ids). Operator verification recovery also requeues the submission. An absent queue is empty for deployment compatibility; a malformed present queue fails closed.

The existing ten-minute maintenance schedule checks up to 25 queued submissions per pass and rotates unfinished or broken records behind the remaining work. Each pass uses one commit for all receipts, deduplicated principal rate changes, and queue cleanup, avoiding bursts of content-creating GitHub requests. A completed registration restores the interval to sixty seconds; the existing limited preparation-failure concession uses the same path. Other closed submissions leave the cooldown unchanged and leave the queue. Failures remain queued and are reported without preventing unrelated resets. Transport or authentication failures abort the batch without continuing to write. A status poll uses this same reset path as a fast path, but keeping a tab open is unnecessary. Before enforcing an active cooldown or doubling an expired interval above the floor, the server checks the authenticated principal's newest 25 queued outcomes directly. Returning submitters therefore do not need to wait for a full maintenance-queue rotation.

The rate change and the submission's rate_reset_at receipt commit atomically. A rejected write cannot mark an unapplied reset complete. Concurrent status polls and admissions retry against a fresh snapshot; a receipt already committed by another request prevents applying that same reset to a later submission. A delayed historical registration whose completion predates the current rate record's last start is acknowledged without erasing that newer cooldown.

Install State validation support for index/rate-resets.json before deploying this server change. An initial queue may be installed with that migration; the older server ignores it. For submissions admitted before this queue existed, seed it from a fresh State checkout, and repeat after deployment to catch any admissions during the deployment gap:

node tools/rebuild-rate-reset-queue.js ../PalomarSubmissionState
node tools/rebuild-rate-reset-queue.js --write ../PalomarSubmissionState

The default is a dry run. The write updates only the queue, preserves existing entries, and adds unfinished submissions and unacknowledged registration or preparation resets with numeric principals. It neither needs the pepper nor changes cooldowns itself. Run python3 tools/validate_state.py . in the State checkout, then review and push the queue; scheduled maintenance catches up in bounded batches. Do not replace a live queue from a stale checkout: commit and push without force, and rebuild on a fresh checkout if the push loses a race.

A persistently malformed record remains queued and is reported as a maintenance failure. Repair its document, or remove its id from index/rate-resets.json after reviewing why its reset cannot apply. Validate the full State checkout before committing that queue edit; other entries must remain intact.

Operating a submission

The scheduled workflow in the private PalomarSubmissionState repository runs palomar-review auto, which is one command that advances everything in the queue. The commands below are the manual and recovery interface for the same pipeline, one submission at a time.

palomar-review list                                  # what is awaiting review
palomar-review run --submission <id> --engine codex  # writes no state
palomar-review run --submission <id> --engine codex --apply   # deliver privately
# the submitter decides, on their status page
palomar-review register --submission <id>             # only after consent
palomar-review finalize --submission <id> --pr <n>   # after the database PR merges

run without --apply still runs the review: it calls the model, spends money, and writes a workspace. What it does not do is touch the state repository, which is the only thing --apply adds.

Releasing a submitter from an accumulated backoff

The backoff doubles without a ceiling, so somebody who starts submissions and never finishes one can end up waiting a length of time nothing they can do will shorten. Deleting their rate document puts them back at the floor. Finding it needs TOKEN_PEPPER, because the file is named by a peppered digest of the submitter's numeric GitHub id and its body names nobody:

TOKEN_PEPPER=... tools/rate-path.js --login someone   # resolves the id with `gh`
TOKEN_PEPPER=... tools/rate-path.js --id 4242         # when the id is known

It prints one index/rate/<digest>.json path and nothing else. Delete that file in a clone of the state repository and push. last_start_at in the document is what tells you when this submitter last started, if you want to confirm the file before deleting it.

The pepper is read from the environment and there is deliberately no argument for it, because a shell history and a process listing both outlive the run. It is never printed, including on the failure paths, and gh is spawned without it.

Retiring identifying fields from older rate documents

Rate documents written before the Server stopped recording identity carry a cleartext login, a submission_ids list, or both. A registration reset drops them the next time it touches a document, but a submitter who never registers again leaves theirs in place, so the remainder wants one pass over a clone of the state repository:

tools/strip-rate-logins.js ../PalomarSubmissionState           # report only
tools/strip-rate-logins.js ../PalomarSubmissionState --write   # rewrite in place

Review the diff, commit, and push. It needs neither the pepper nor the GitHub API, and it retires exactly the fields the contract tolerates, from the same list in src/admission-contract.js. It is deliberately not the validator: it removes those fields and leaves everything else exactly as it found it, because it edits files by hand outside the Worker.

Removing a field means reserializing the document, which is not in general a byte-preserving operation. So it rewrites a file only when the bytes on disk are already exactly the canonical serialization of what they parse to, which is what the Worker writes. A document that is formatted differently, or that has duplicate keys or an integer too large to survive a JSON round-trip, is reported and left alone, and the run exits nonzero: rewriting it would change more than the field being removed. Those are for a human to edit. Running it again after a successful pass changes nothing.

It is not erasure: the state repository's git history retains the old bodies, and what happens to those is a question for the state repository's retention policy rather than for this pass.

Scrubbing submissions withdrawn before the scrub shipped

A withdrawal empties the record it closes, but only from the write that closes it. A submission withdrawn before that shipped kept its submitter, its notes, its authorization evidence and its principal's login, and nothing later goes back for them. The same one pass over a clone retires those:

tools/scrub-withdrawn-records.js ../PalomarSubmissionState           # report only
tools/scrub-withdrawn-records.js ../PalomarSubmissionState --write   # rewrite in place

It touches only records already withdrawn, empties exactly the fields withdrawnRecord empties, and appends the scrub event that record would have carried, stamped with its own status because that is what the State validator asks of a last event. A record already scrubbed is left alone, so running it again changes nothing.

The same caveat applies: history keeps what the tip no longer shows.

Rotating the state pepper

An account migration rotates TOKEN_PEPPER, invalidating every old raw status link. GitHub identity recovery continues to work because current records retain the submitter's numeric principal. Rebuild the pepper-derived locator and reset rate history in a fresh State checkout, first as a dry run:

TOKEN_PEPPER=... tools/rekey-pepper-indexes.js ../PalomarSubmissionState
TOKEN_PEPPER=... tools/rekey-pepper-indexes.js --write ../PalomarSubmissionState

The command refuses to write if any nonclosed record lacks a recoverable principal. It deliberately preserves index/tokens/: those historical pointers are required by the State repository's integrity relation even though the new pepper makes their corresponding raw tokens unusable. Validate and review the complete State diff before merging it.

Deploying

Pushes to main are deployed automatically after the test suite passes. The GitHub repository must provide CLOUDFLARE_ACCOUNT_ID as a repository variable and CLOUDFLARE_API_TOKEN as an Actions secret. The account variable is public configuration and must equal the account_id pinned in wrangler.jsonc. The Cloudflare dashboard credential stored under the token secret is named exactly palomar worker deployment token. It is not PalomarDatabaseTools GitHub deploy, which is a separate credential owned by a different repository. palomar worker deployment token must not have Workers Routes: Edit: this deployment deliberately uploads and promotes versions without changing the existing route. Before uploading a version, CI reapplies the reviewed Worker hostname policy; it then uploads and promotes that version. The version workflow does not change the existing route or cron trigger.

The Worker imports no package from npm at runtime. The locked npm graph is development tooling: Wrangler and the official Vitest pool's Miniflare/workerd stack used to test, bundle, and deploy the Worker. That still handles source code and, during the deployment steps, runs next to production credentials, so CI rejects every low-or-higher advisory in the complete development graph. A least-permission weekly workflow repeats that registry audit directly from the lockfile, without running dependency install hooks, so an advisory published after the last code change does not wait for another pull request; it can also be run manually. The required audit includes the runtime test harness and its Vitest graph, so an advisory there also blocks deployment.

npm test remains the fast Node contract suite. npm run test:runtime is the small required integration layer: it loads both real Wrangler configurations in workerd, invokes the actual Worker entrypoints, and intercepts representative GitHub traffic at the runtime fetch boundary. CI requires both before the deployment job can run; its runtime tests receive dummy required bindings, no credentials, and no remote services. An outbound-service guard refuses any request that a test does not explicitly intercept. The pinned workerd/Vitest graph requires Node 22.12 or later. When advancing a Worker's compatibility date, also advance the pinned Wrangler when needed so its local workerd supports that date.

npm run lint applies the locked ESLint recommended correctness rules to every shipped Worker and browser module, the Node and workerd tests, and their JavaScript configuration. It catches invalid or unreachable code, unresolved globals, and unused bindings across those environments. This is deliberately a lint gate, not a claim that the untyped JavaScript has been statically type-checked; it parses import declarations but does not resolve their targets. The lint packages are part of the complete audited development graph described above, so a future low-or-higher advisory in that graph also blocks deployment.

Before the first deployment against a State repository, commit all three files from state-bootstrap/index/ as described above. Admission and scheduled reconciliation validate the contracts before using them; /healthz stays a network-free configuration check so public monitoring cannot spend the shared GitHub API budget.

Merge the State reporting change and run Refresh private operational report to create reports/dashboard.json on State main before merging this Server change. A Server merge deploys automatically; if the report is not ready, the authenticated route fails closed with a typed 503 until it appears. No OAuth application callback change is needed: dashboard and intake both return through the existing /oauth/callback, and their bound state values are disjoint.

To deploy manually:

npm ci
npm run audit:dependencies
npm run lint
npm test
npm run test:runtime
npx wrangler deploy --dry-run
npx wrangler deploy
curl -s https://submit.palomar-registry.org/healthz

Both Worker configurations explicitly disable workers.dev and versioned or aliased preview URLs. The reviewed hostnames are their only public routes; do not enable another hostname without giving it the same exposure review. The main deployment applies just this account-level hostname policy before every version upload, while the redirect deployment applies its complete configuration through wrangler deploy.

The separate, secret-free palomar-domain-redirect Worker owns palomarregistry.org and www.palomarregistry.org. It permanently redirects every path and query string to the same URL at palomar-registry.org:

npx wrangler deploy --config redirect/wrangler.jsonc --dry-run
npm run deploy:redirect
curl -sSI https://palomarregistry.org/about.html

Classification snapshots

The guided metadata form serves the checked-in arXiv and MSC 2020 taxonomy snapshots from public/taxonomies/. Their sources, retrieval date, and third-party licensing terms are recorded in public/taxonomies/LICENSE.md. Keep that notice with the assets when updating or redistributing them.

Preliminary Lean source checks

Every regular .lean source file in the submitted repository must use Lean's module system and contain at most 10,000 physical lines. This includes Challenge, Solution, unused source files, generated certificates, contained projects, and local path dependencies. Ordinary comments may precede the module header; module documentation belongs after it. Blank and comment lines count. LF and CRLF each delimit one line; an unterminated final line counts, and a final newline does not add an empty line.

Lake configuration files named lakefile.lean are exempt from the module header requirement, but still have the 10,000-line cap. Files below .git or .lake are excluded; submitted .lean symbolic links are rejected so a link cannot hide an oversized source file. Separately declared substantive source repositories for thin wrappers receive the same checks. External pinned Git dependencies are outside this per-file limit; Lean still checks their compatibility with the module system. The existing Challenge limits of 1,000 lines and 100 KiB also apply.

Porting requires more than adding module: make the declarations needed by other modules public, use public import where the public interface needs an import, and expose definitions whose bodies clients need. See Lean's modules and visibility reference. Rebuild and rerun Comparator after porting. Split oversized files into smaller modules or reduce generated certificates; do not hide them in excluded paths.

The submission form and HTTPS intake check a bounded subset of the submitted repository at the exact commit and report incomplete scans explicitly. They do not scan separately declared substantive repositories; preparation checks those. The verifier scans the complete checkout before builds and confirms headers with Lean's parser before running submitted Lake code. Violations identify the file and require a corrected new commit. These rules apply to new ordinary submissions and revisions; metadata corrections retain their registered source and are not retroactively rejected.

Browser and API scans inspect at most 32 files, with 1 MiB per file and 4 MiB of source in total, largest files first. They can diagnose a missing module header or more than 10,000 completed lines from a bounded prefix. Otherwise a partial read is incomplete. The API bounds its tree response to 2 MiB and its scan to 10 seconds. No GitHub credential is sent to the public raw-content host. API violations return HTTP 400 before a pending intake or authentication challenge is created. Successful ordinary intake JSON includes source_preflight with status (pass or incomplete), files_checked, and, when the tree was read, total_files. Even pass is preliminary.

Rollout: merge the policy and verifier changes before deploying this server change; the browser's current-policy check and CI contract use the verifier's published projection. Deploying the server alone cannot enable the complete verification gate.

Resource retries

Automatic resource retries are restricted to the GitHub accounts in src/resource-escalation-policy.json. The list starts empty. To grant access, resolve the account with gh api users/LOGIN --jq '{id, login}', add that object to users, and deploy through the normal reviewed PR process. The numeric GitHub ID is authoritative; the login documents the entry and may change without granting another account access. Adding an entry also authorizes a retry of that account's earlier eligible failures when their status page is next opened, subject to the normal in-flight limits; it does not bulk-retry the account's history. Removing the entry stops future automatic retries. The audit field policy_sha256 hashes UTF-8 JSON.stringify of the parsed policy (compact JSON with no trailing newline), rather than the source file's formatting.

Only a full verification with Reviewer-authenticated OOM or timeout evidence on palomar-namespace-16x32-v1 can automatically retry. It gets one fresh attempt on palomar-namespace-32x64-v1 (32 CPU / 64 GiB), with the same 19,800 second execution budget and the same admitted commit, authorization, registration consent and cooldowns. The original run and failure remain in execution history. A later operator retry cannot reset the automatic retry allowance. There is no 64×128 tier enabled.

The scheduled reconciler queues the retry without requiring an open browser. It retains admission capacity until Reviewer has authenticated the failure, and reserves State, reviewer queue and any needed capacity in one conditional commit before the durable dispatcher creates work. Competing writes require another pass; withdrawal wins if it precedes the retry commit. A failure is never inferred from candidate output, exit code 137 or a generic interrupted job.

Resource failures tell submitters to contact Kim Morrison (@kim-em on GitHub), with their workflow URL, to request larger-runner access. Repeated submission does not grant access. If the larger attempt fails too, it stops for operator attention. Manual Technical Maintainer recovery remains available.

Deployment prerequisites: merge the resource evidence changes in PalomarReviewer, promote that runtime and the execution contract in PalomarState, and enable the 32×64 profile in PalomarSubmission before deploying this server. Keep the allowlist empty until those prerequisites are deployed.

About

Palomar submission server: a Cloudflare Worker over GitHub state

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages