Servers
vx runs and caches on its own. Three servers add what one machine cannot. You host each one, on Cloudflare Workers, Bun or Docker:
| Server | Holds | Its plugin | Page |
|---|---|---|---|
@vzn/vx-cache-server |
task artifacts, in one S3 or R2 bucket | vxCache(), @vzn/vx-cache |
Cache server |
@vzn/vx-history-server |
task timings, one SQLite or D1 row per task | vxHistory(), @vzn/vx-history |
History server |
@vzn/vx-dte-server |
nothing: it relays a distributed run’s messages | dte(), @vzn/vx-dte |
DTE server |
No server stores a secret. Every request carries the CI job’s own OIDC token (GitHub Actions or GitLab CI), and the token decides what the request may do. Off CI there is no token, so the plugins run locally.
All three with Docker Compose
Section titled “All three with Docker Compose”private/vx-servers/compose.yaml
builds and runs the three from a checkout of the vx repository:
cd private/vx-serversdocker compose up -d # all threedocker compose up -d cache history # only the ones you need| Service | Port | Settings it reads |
|---|---|---|
dte |
8080 (DTE_PORT) |
DTE_ALLOW, DTE_AUDIENCE |
cache |
8081 (CACHE_PORT) |
TRUST, AUDIENCE, S3_*, MAX_* |
history |
8082 (HISTORY_PORT) |
TRUST, AUDIENCE, TIMING_DAYS |
Settings come from your shell or a .env file next to compose.yaml.
The relay keeps its runs in the dte volume, and the history server keeps
its SQLite file in the history volume. Put TLS in front of each server:
a CI token is a bearer token.
TRUST: who reads and who writes
Section titled “TRUST: who reads and who writes”The cache and history servers share one token check
(@vzn/vx-devtools).
TRUST is a JSON list of rules. Each rule names:
iss, the issuer:https://token.actions.githubusercontent.comor your GitLab’s URL;- the repository by an id that a rename keeps:
repository_idon GitHub,project_idon GitLab; - the refs it protects:
refon GitHub,ref_pathon GitLab; - any other claim the writing job must hold.
A value is a string, where * matches anything, or a list of strings.
[ { "iss": "https://token.actions.githubusercontent.com", "repository_id": "123456789", "event_name": ["push", "merge_group"], "ref": ["refs/heads/main", "refs/heads/gh-readonly-queue/main/*"], "ref_protected": "true" }, { "iss": "https://gitlab.com", "project_id": "4567", "ref_path": "refs/heads/main", "ref_protected": "true", "pipeline_source": ["push", "schedule"] }]What a token may do:
- Its repository only. It reads and writes the repository it names.
A repository no rule names is refused (
403). WithTRUSTempty, every request is refused:the server trusts no CI: set TRUST. - It writes its own branch only. A job writes the scope of its own
ref: on GitHub a pull request’s head branch, a merge queue’s base
branch (its ref
refs/heads/gh-readonly-queue/<base>/pr-<n>-<sha>, that exact form: its code is about to become the base), elseref; on GitLabref_path. No header or client option can change that. - Protected refs. A ref a rule names, such as
refs/heads/main, is protected. Only a job that matches every claim of the rule writes it. Any other job on that ref only reads, so a main run never reads a write it did not make. - No tiers. A scope is a ref’s hash (the first 128 bits of its SHA-256, in hex) and nothing more; no server sees a ref plain. A read names the scopes it wants, in order, own branch first, and the server answers from the first one that holds the key.
Always name event_name. pull_request_target and issue_comment runs
carry main’s ref while they run a pull request’s code. A merge queue’s
run writes main’s scope before its code lands (with merge_group in
main’s rule): code you merge or queue is code you trust. Run security
checks on the pull request, before it is queued.
AUDIENCE is the aud a token must name. By default it is the origin
the server is reached at, which is what the plugins ask GitHub for.
The relay takes CI tokens too, with its own list, ALLOW
(DTE server).