Choosing a store
Where vci keeps attestations, which store to pick, and how to set up each one. Since 2.8.0 there are four; git-meta stays the default, and a repository that configures nothing works as before.
vci run signs one attestation per unit that passed and stores it with the input manifest it refers to. vci plan reads them back, verifies them, and skips a unit when one proves its inputs unchanged. The attestation store is where those records live.
The store is never trusted. vci plan checks every attestation's signature against the base commit's .vci/allowed_signers and identities, and recomputes every input manifest's root, whichever store held it. A record that is forged, altered, replayed or deleted can only make a unit run. Write access to a store therefore matters for availability only: a writer can make CI slower by deleting records, and cannot make it skip a test it should run. Read access shows test ids, signer ids and input manifests (paths and content hashes), so give it to whoever may read the repository.
The four stores
| Store | Where the records are | Server | Pick it when |
|---|---|---|---|
| git-meta (the default) | .git/git-meta.sqlite, shared by the clone's worktrees, exchanged on refs/meta/main of a git remote | none: git is the transport | You want nothing new to run or pay for. |
| SQLite | one file, .git/vci-store.sqlite | none | One machine: a laptop, a single long-lived runner, trying vci out. Nothing is shared unless you copy the file. |
| PostgreSQL | tables in schema vci, one namespace per repository | a PostgreSQL server you run | You already run PostgreSQL and want every machine to see every attestation the moment it is stored. |
| libSQL (Turso) | tables per repository in a libSQL database: Turso's hosted service or a sqld of your own; optionally read through a local replica | Turso runs it, or your own sqld | The same, without running a database server yourself. |
What fetch and push mean
- git-meta writes to a local store.
vci pushpublishes it onrefs/meta/main, andvci fetch(orvci ci --fetch, as the action runs it) brings in what others pushed. On a busy repository that fetch and merge is the largest part of a plan: a median of 7.4 of 15 seconds on vex's Rails pull requests on 24 and 25 October 2026 (benchmarks). - SQLite has nothing to exchange.
vci fetchsaysthe sqlite store at … is local: nothing to fetch (N attestations stored),vci pushsays there is nothing to push, both with exit 0. A workflow that uses the action keeps working. One without it must drop itsvci fetch --remote originstep, as on the shared stores below. - PostgreSQL and a libSQL server are shared.
vci runwrites to the server at once, and every reader sees it with no fetch.vci fetchandvci pushsay the store is shared and exit 0; the action's--fetch originand--remote originare ignored with a warning.vci fetch --remoteandvci push --remoteexit 2 on any store but git-meta, so a workflow without the action drops itsvci fetch --remote originstep (step 5 below).
On a shared store, vci ci stores nothing without --push. With git-meta nothing a CI job attests leaves it unless the job pushes, and the shared stores keep that rule. Without --push it signs, then prints:
vci: not storing 2 attestation(s): the libsql store is shared and vci ci was not given --push
vci ci: 2 unit(s) attested (not stored: the libsql store is shared and --push was not given)
The action passes --push only with signing-key or oidc (and push: true, the default), so in practice only the job you give CI's key stores anything. A developer's vci run always stores.
What each costs
Measured on synthetic stores on a laptop in October 2026, and on vex's Rails suite against Turso's hosted service on 27 October 2026; the benchmarks page has the tables and their caveats.
- In a plan that skips 1,200 units, reading the attestations took 0.01 s from a SQLite file. From a libSQL server on the same machine, reading them took four requests and 0.42 to 0.57 s, and the whole plan about a dozen requests (the status, the flags and the memo too). A PostgreSQL plan of 1,200 units sends 14 statements.
vci runstored 1,200 attestations and manifests in one request (0.14 to 0.25 s) on libSQL, and in one transaction (0.20 to 0.76 s) in SQLite.- Across the internet each request pays a round trip, and the values' bytes add more. From vex's GitHub runners to Turso in aws-eu-west-1, a plan of 2,291 Rails units that read 1,638 attestations took 4.8 to 10.1 s, with no cache to restore and nothing to fetch or push. On the same runners, vex's git-meta jobs spent a median of 44 s restoring their cache, putting it in place and planning. Storing about 810 attestations took 22 to 42 s into Turso, with nothing to push after it. On the same host class, git-meta's local store took 9 to 18 s and the push after it 32 to 63 s. The Turso namespace held at most 3,276 attestations, where vex's git-meta holds about 24,000 (the pilot).
Which store a repository uses
vci picks the store once per command, from the first of:
VCI_STORE:git-meta,sqlite,postgresorlibsql. Never a URL: a value with://is refused and not printed.- The scheme of
VCI_STORE_URL:postgres://orpostgresql://,sqlite:,libsql://. Anhttps://URL needsVCI_STORE=libsqlbeside it. [store] backendin the working tree'svci.toml.- The scheme of
[store] urlthere. - git-meta.
vci store info says which store was picked, where each setting came from, whether the store can be read, and how many attestations it holds. It never prints a password or a token, so its output is safe to paste into an issue or a chat:
$ vci store info
store: git-meta
location: .git/git-meta.sqlite (exchanged on refs/meta/main)
settings:
backend: git-meta (the default)
connect_timeout: 5s (the default)
timeout: 30s (the default)
capabilities: shared writes no, lazy values yes, keeps hints yes
exchange: vci fetch and vci push, on refs/meta/main
status: ready
attestations: 4
It exits 0 when the store can be read, also before anything is stored, and 2 otherwise, after printing what it knows and the error. --format json gives the same as JSON. Run it first whenever a plan says reading attestations from ….
Keeping the store's credentials safe
A shared store has a credential: a Turso token, or a PostgreSQL password. vci handles it like this, and asks the same of you.
- Never a credential in
vci.toml. Every checkout carries it, a pull request's included. vci refuses a[store] urlholding a password, a token, anauthTokenor a#fragment, without printing it.vci.tomlmay say where the server is ([store] url) and which command prints the credential ([store] token_command). - A token from the environment goes only to a URL from the environment.
VCI_STORE_TOKENwith the URL fromvci.tomlis refused, because a pull request could change that URL to a server of its own and collect CI's token. CI sets bothVCI_STORE_URLandVCI_STORE_TOKEN, and so does a machine given a read-only token of its own (step 6); a laptop that reads the token throughtoken_commandsets neither. - Tests can still find the token. vci removes
VCI_STORE_URL,VCI_STORE_TOKENandVCI_STOREfrom its own environment as it starts and from every environment it builds for a test, probe, hook or token command. The environment a process started with stays readable to every process of the same user, though (/proc/<pid>/environon Linux,ps ewwon macOS), and so does the CI step's shell, which set the variables. vci cannot hide them there. - So a job that runs untrusted code gets a read-only token, or none. Only trusted jobs hold a token that can write: the default branch's, or pull requests from the same repository, whose authors could change the workflow and read its secrets anyway.
- A credential covers every repository in its database. Namespaces keep each repository's records apart, but a Turso token, or a PostgreSQL role granted on schema
vci, reads the test ids, file paths and content hashes of every repository there, and one that can write can delete them. Use one database per repository, or per group of repositories with the same readers (on PostgreSQL, a database or a schema of its own,[store.postgres] schema, each with its own roles). - On your own machine,
[store] urlis as sensitive astoken_command. The token the command prints goes to whatever[store] urlnames, and both come from the checkout. Review a change to either as you would a change to a test helper. In a checkout you have not reviewed, run vci withVCI_STORE=git-meta: it then ignores both and never runs the command. - Never print a token. Pipe it from
turso db tokens createstraight intogh secret setor your secrets manager, and never pass it as a command-line argument, where it lands in shell history and process listings. To see why a token command fails, run it yourself, in your own terminal, with its standard output discarded (below). That discards the token a command that works prints on standard output. Its standard error still shows, and a wrapper or a pipeline stage can log the token there, so never have an agent run it, or any other command that reads a secret: an agent tells you what failed and the command.
Set up Turso (libSQL)
Turso hosts libSQL databases, so this is the shared store with no server to run. One database can hold many repositories, each in tables of its own, but its tokens open all of them, so give a database to each repository, or to each group of repositories with the same readers (above). The steps below set up CI on GitHub Actions and the laptops and agents that attest.
-
Install the Turso CLI and sign in
On macOS:
brew install tursodatabase/tap/tursoOn Linux:
curl -sSfL https://get.tur.so/install.sh | bashThen sign in, adding
--headlesson a machine without a browser:turso auth login -
Create the database
turso db create vci turso db show vci --urlAdd
--group <name>toturso db createif your organization has more than one group.turso db showprints the URL,libsql://vci-<organization>.<region>.turso.io. The URL is not a secret: it says where the database is, and the token is what lets a client in. Create an ordinary database, without--tursodb: a TursoDB (MVCC) database's URL starts withturso://, which vci does not accept ($VCI_STORE_URL is not a libsql location, or, withoutVCI_STORE=libsql, a warning that git-meta ignores it). -
Decide who gets which token
Token Made with For can write turso db tokens create vcithe default branch's CI job, which stores what it attests; developers whose vci runshould store attestations; an agent's sandbox that should store, through a token of its own (step 6)read-only turso db tokens create vci --read-onlypull-request jobs, and any machine that only plans or runs code you have not reviewed, an agent's sandbox included. Its plans read the store; its writes are refused ( libsql store refused to write: … the token may not write) and the tests' exit code is keptTokens do not expire unless you say so:
--expiration 90dsets a lifetime in days. An expired or revoked token makes the store unavailable, and every test then runs until you replace it.turso db tokens createprints the token alone on standard output, so pipe it to where it is kept (next step) instead of copying it from the screen. -
Store CI's values as GitHub secrets
Create the environment
vci-attest, limited to the default branch, if CI does not sign with a key of its own yet (attesting in CI shows how). Then, from a clone of the repository:turso db show vci --url | gh secret set VCI_STORE_URL turso db tokens create vci --read-only | gh secret set VCI_STORE_TOKEN turso db tokens create vci | gh secret set VCI_STORE_TOKEN --env vci-attestgh secret setreads the value from standard input when it has no--body, so the token never appears on screen or on a command line. A secret of an environment wins over a repository secret of the same name in the jobs that use that environment: pull-request jobs get the read-only token, and the main-branch job, which runs invci-attest, gets the one that can write. Pull requests from forks get no secrets at all, and those Dependabot opens get only Dependabot's secrets. vci treats an empty variable as unset, so their jobs fall back tovci.toml. Before[store]is committed (step 6) that is git-meta: the action fetchesrefs/meta/main, and the attestations already there, verified as always, can still skip units. Once it is committed, vci runs itstoken_command, which is neither installed nor signed in on GitHub's runners, so the plan saysrunning everything: … the token_command (…) was not foundand every test runs. -
Give both jobs' vci step the two variables
The same two lines in each job, on the vci step. The main-branch job also attests (
signing-key, oroidc: true), and the action then runsvci ci --key … --push, which stores what it attested. On a shared store--pushwrites to the database, not to git, so that job needs nocontents: write.name: test on: pull_request: push: branches: [main] permissions: contents: read jobs: # Pull requests: read the store, skip what is attested, store nothing. test: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # the base commit, and the repository id the store's tables are named after # ...your toolchain steps... - uses: PandelisZ/cryptographically-verifiable-ci-runner@v2 env: VCI_STORE_URL: ${{ secrets.VCI_STORE_URL }} VCI_STORE_TOKEN: ${{ secrets.VCI_STORE_TOKEN }} # the repository secret: read-only # Pushes to main: run what is not attested, attest it, store it. test-main: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: vci-attest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # ...your toolchain steps... - uses: PandelisZ/cryptographically-verifiable-ci-runner@v2 env: VCI_STORE_URL: ${{ secrets.VCI_STORE_URL }} VCI_STORE_TOKEN: ${{ secrets.VCI_STORE_TOKEN }} # vci-attest's secret: can write with: signing-key: ${{ secrets.VCI_SIGNING_KEY }} # or oidc: true (with permissions: id-token: write)Every step of a job can read the job's
env. On the vci step, the variables reach vci (the action is a composite action, whose steps inherit them) and no earlier step, so the toolchain and setup actions never see the token that can write. The tests run inside the vci step, though, and can read what it was given (see the box above), which is why the pull-request job's token is read-only.Without the action, give the two variables to the step that runs
vci ci, runvci ci --base-ref "$BASE" --key "$RUNNER_TEMP/vci-key" --pushin the main-branch job, with the key written to that file as the workflowvci initprints writes it ((umask 077 && printf '%s\n' "$KEY" > "$RUNNER_TEMP/vci-key")). Never pass the key itself as--key's value: vci takes one that starts with-----BEGINfor an unexpected argument and prints it whole in the error, into the job's log. Leave out--pusheverywhere else. Remove avci fetch --remote originstep if the workflow has one, as the workflowvci initprints and this repository's examples do: on any store but git-meta it stops the job withvci: error: the libsql store does not support a remote(exit 2) before any test runs.vci ci --fetch originonly warns, so it can stay. -
Point laptops and agents at it
Developers read a token through a command named in
vci.toml, signed in to its tool on their own machines, so that nobody exports anything; an agent working in a developer's own session reads it the same way. An agent's sandbox gets a token of its own (below). First put a token that can write where that command will read it, from standard input as before. In Composal, with--from-stdin, never--value, and without--ci, which would hand the secret to CI runners as an environment variable:turso db tokens create vci | com secrets set vci-store-token --org acme --from-stdin --env-key VCI_STORE_TOKENOr in 1Password, where
op item creategiven-reads the item as JSON on standard input:turso db tokens create vci \ | jq -R '{title: "vci-store", category: "API_CREDENTIAL", fields: [{id: "credential", label: "credential", type: "CONCEALED", value: .}]}' \ | op item create --vault Engineering -Then commit a
[store]table that names the database and that command:[store] backend = "libsql" url = "libsql://vci-acme.aws-eu-west-1.turso.io" # never a credential token_command = ["com", "secrets", "get", "vci-store-token", "--org", "acme"]token_commandis an array run directly, without a shell, or a string run withsh -c. It runs only when a command first reads the store, at most once per vci process, in the repository's top level, and must print the token alone on one line within 60 seconds. It cannot prompt, so sign in to the tool first. Some choices:# Composal: each read of the organization's secret is audited; admins only token_command = ["com", "secrets", "get", "vci-store-token", "--org", "acme"] # 1Password: anyone who can read the vault token_command = ["op", "read", "op://Engineering/vci-store/credential"] # a file: weaker (below). A string runs with sh -c, so ~ is expanded token_command = "cat ~/.config/vci/store-token"com secrets getreads an organization's secret for its admins only (its help says "admin only, audited"). A developer who is not an admin gets a failing command, and their plans run everything. If such developers should store attestations too, keep the token where they can all read it, such as a shared 1Password vault.A file is the weakest choice: the token sits on disk, readable by anything you run, never audited, and signing out does not revoke it. If you use one anyway, write it without showing it:
mkdir -p ~/.config/vci && (umask 077; turso db tokens create vci > ~/.config/vci/store-token). In an array,~is not expanded: give the full path.One committed
token_commandgives every machine the same token, through a person's own sign-in. A machine that should only plan, or that runs code nobody has reviewed, such as an agent's sandbox, gets a token of its own through its environment instead, made for this database alone, with--read-onlyunless that machine should store. Never sign such a machine in tocomor to a person's 1Password account: the code it runs could then read every secret that account can, the token that can write included. Set both variables where its session starts, from the sandbox's own secret settings, or read from a 1Password service account allowed only a vault that holds nothing but that token, never the Engineering vault above, which holds the token that can write. For example, withOP_SERVICE_ACCOUNT_TOKENset:export VCI_STORE_URL=libsql://vci-acme.aws-eu-west-1.turso.ioandexport VCI_STORE_TOKEN="$(op read op://vci-sandbox/vci-store-read-only/credential)". Both are needed (a token from the environment is never sent tovci.toml's URL), and they win over[store]. Every process of that user can read them, its tests included, which a token that can only read allows for.In CI the two variables win over
[store], and the command does not run whileVCI_STORE_TOKENis set and not empty. GitHub gives a secret that does not exist (a fork's pull request, a secret never set) as an empty string, which vci treats as unset: the command then runs in CI, and fails where its tool is not installed or not signed in (troubleshooting). Do not sign a CI runner in to that tool, or such a job would get the token that can write.A vci before 2.8.0 rejects a
vci.tomlwith[store]and runs every test, so commit the table only once every runner and developer has 2.8.0 or later; until then, select the store through the environment. -
Check it
vci store infoAn uncommitted
[store]in the working tree is enough for this check, since vci reads the working tree'svci.toml. Before anything is stored it says:store: libsql location: libsql://vci-acme.aws-eu-west-1.turso.io namespace <repository id> (remote) settings: backend: libsql (vci.toml [store]) url: libsql://vci-acme.aws-eu-west-1.turso.io (vci.toml [store]) mode: remote (vci.toml [store]) token: (from token_command: com secrets get vci-store-token --org acme) (vci.toml [store]) namespace: (the repository id) (the default) connect_timeout: 5s (the default) timeout: 30s (the default) capabilities: shared writes yes, lazy values no, keeps hints yes exchange: none (vci fetch and vci push have nothing to do) status: absent: nothing is stored yet (the first `vci run` creates it) attestations: 0In CI the token line says
token: (set) ($VCI_STORE_TOKEN). The token itself is never printed. -
Expect the first runs to run everything
The store starts empty. Until something is stored, a plan warns
no attestations are available in the libsql store (the first `vci run` creates it)and every unit runs. The default branch's next full run fills it:no_skip_refsmakes that run run everything, and with CI's key it attests every unit that passes and stores it. Developers' and agents'vci runadd theirs. The first write with a token that can write creates the repository's tables; a read-only token cannot (libsql store is not set up: the first vci run with a token allowed to create tables creates them).Switching store copies nothing and invalidates nothing. The attestations on
refs/meta/mainstay there, read only by a job that falls back to git-meta (a fork's pull request before[store]is committed), and each unit runs once more before the new store has it. There is no command that copies a store yet. -
Check CI
After the first push to main, the main-branch job's log has a line per stored unit (
vci: attested … as libsql …) andvci ci: N unit(s) attested. If that line ends(not stored: the libsql store is shared and --push was not given), the job attested but stored nothing: the action haspush: false, or, without the action,vci ciwas not given--push. A job with neithersigning-keynoroidcattests nothing and prints no such line. The plan of the next pull request showsSKIPlines for what main attested, and norunning everything: reading attestations from libsql.Nothing in CI shows that the repository secret is read-only, since pull-request jobs never write. If you are not sure it came from
--read-only, make every token of the database invalid withturso db tokens invalidate vci, then set the secrets of steps 4 and 6 again.
Replica mode
A machine that keeps its clone, such as a laptop or a self-hosted runner, can read a local copy of the database instead of asking the server for every plan:
[store.libsql]
replica = true # or VCI_STORE_SYNC=replica in the environment
The copy is .git/vci-store.libsql. Each command that reads the store first syncs what changed since the last sync, in one request; with nothing new that took 0.01 to 0.04 s against a local server. Writes go to the server, then to the copy. A sync that fails prints vci: warning: cannot sync the libsql replica …; reading the replica as it is, and plans and runs go on with the copy as it was, which can only make units run. An ephemeral runner starts with no copy, and its first sync reads the whole namespace, about a millisecond a record (1.4 to 4.4 s for 1,600 to 6,500 records from vex's runners to Turso), a cost that grows with the namespace, while a remote plan reads only the attestations it checks. On those fresh runners the replica was no faster than remote mode (the pilot): keep it for laptops and long-lived machines.
Namespaces and shallow clones
Each repository's tables are named after its namespace: by default the repository id, its first commit (git rev-list --max-parents=0 HEAD). Forks and mirrors share it; unrelated repositories never do. A shallow clone cannot know its first commit, so there vci refuses the default:
attestation store: the libsql store keeps each repository's records under a namespace, by default the repository id (its first commit); this is a shallow clone, whose first commit is where the clone was cut: set $VCI_STORE_NAMESPACE (the same value wherever the store is shared)
Keep fetch-depth: 0 in CI, which vci needs for the base commit anyway, or set VCI_STORE_NAMESPACE to the same value everywhere. [store.libsql] namespace sets it in vci.toml.
When it does not work
A store that cannot be used fails open. vci plan prints a warning and running everything: reading attestations from libsql: …, every test runs, and nothing is skipped by mistake. vci run and vci ci run the tests, refuse to store each unit with the reason, and keep the tests' exit code. vci store info, vci fetch, vci push and vci prune exit 2 with the error.
| Message (shortened) | What it means |
|---|---|
the server asks for a token: set $VCI_STORE_TOKEN (or [store] token_command) (HTTP 401) | No token reached vci, and vci.toml has no token_command. In CI: VCI_STORE_URL is set but VCI_STORE_TOKEN is empty (the secret was never set, or is not in the vci step's env:). Locally: VCI_STORE_URL is exported without a token. |
the server refused the token in $VCI_STORE_TOKEN or the URL's authToken, or … from [store] token_command (…) | A token for another database, revoked or expired. Make a new one (step 3). |
libsql store refused to write: … the token may not write | A read-only token where vci stores. Expected in pull-request jobs; elsewhere use a token that can write. |
$VCI_STORE_TOKEN is set but the store's URL comes from vci.toml [store] url | The trust rule above. In CI set VCI_STORE_URL too; on a laptop unset VCI_STORE_TOKEN so the command supplies the token. |
vci.toml [store] url carries a credential in its query (authToken, password, ...) (it was not printed) | Take the token out of vci.toml, and make a new one: it was in a file every checkout carries. |
the token_command (…) was not found in CI | The job's VCI_STORE_TOKEN is empty, so vci ran [store] token_command, whose tool CI's runners do not have. A fork's pull request gets no secrets, so it runs everything by design. A pull request Dependabot opens gets only Dependabot's secrets: if its jobs should read the store, set the two there too, the token read-only (gh secret set VCI_STORE_TOKEN --app dependabot, from standard input as in step 4). Otherwise the secret was never set (the repository's, for pull-request jobs) or is not in the vci step's env:: set it (step 4). Running the command on a laptop tells you nothing here. |
the token_command (…) exited 1; vci shows none of its output, which may hold the credential: run the command yourself with its standard output discarded (> /dev/null) to see why | The command failed on your machine. Run it yourself, in your own terminal, as the message says, from the repository's top level: com secrets get vci-store-token --org acme > /dev/null. Only standard output is discarded: the error shows on standard error, and so would the token if a wrapper or a pipeline stage logged it there, so do not run it where others can see the screen, and do not have an agent run it. Usual causes: not signed in (com login, op signin), not allowed to read the secret (com secrets get is for the organization's admins), a command that wanted to prompt, or one stopped after 60 s. This is 2.8.1's message. 2.8.0 says only "run the command yourself to see why": discard standard output all the same. 2.8.0 also leaves most characters a shell reads specially without quotes (&, ;, |, #, <, (, glob characters), so pasted as it is, the shell can end the command or start a comment before the redirect, and print the token: copy the command from vci.toml instead, with every argument in single quotes ('com' 'secrets' 'get' …). A string token_command runs with sh -c, so run it the same way, sh -c '<the string>' > /dev/null, with each ' in it written as '\'': pasted bare, a || b > /dev/null discards only what b prints. |
cannot reach libsql://…, libsql store: the server's first answer timed out after 5s | The server is down or unreachable. Connecting and the first answer take at most 5 s (VCI_STORE_CONNECT_TIMEOUT_SECS), later requests 30 s (VCI_STORE_TIMEOUT_SECS), and after the first failure every later call of the command fails at once, so it costs one timeout per command. |
no libsql database there (HTTP 404) | The URL names no database: check turso db show vci --url. |
the server takes no more writes (blocked, as a database over its quota is, or full) | The database is over its plan's quota or at its size limit. Plans still read it; nothing is stored. vci prune keeps the tables small. |
this is a shallow clone … set $VCI_STORE_NAMESPACE | See namespaces. |
Set up PostgreSQL
vci needs a database and three roles: one that creates the schema, one that writes for trusted CI, and one that reads. The roles cover every repository whose records are in schema vci, so set this up once per group of repositories with the same readers (a database each, or a schema each with [store.postgres] schema).
-
Create the database and the roles
-- in psql, as an administrator CREATE DATABASE ci; CREATE ROLE vci_owner LOGIN; -- creates the schema CREATE ROLE vci_writer LOGIN; -- trusted CI: the default branch CREATE ROLE vci_reader LOGIN; -- pull-request jobs, and anyone who only plans GRANT CREATE ON DATABASE ci TO vci_owner; \c ci REVOKE CREATE ON SCHEMA public FROM PUBLIC; \password vci_owner \password vci_writer \password vci_readerPostgreSQL 14 and earlier, and clusters upgraded from them, let every role create objects in schema
public. Therevci_readercould create a function that vci, run later asvci_writeror any other role, calls in place of a built-in one, with that role's rights. TheREVOKEtakes that away; on a cluster made by PostgreSQL 15 or later it changes nothing.\passwordasks for the new password without showing it and sends it to the server already encrypted, so it lands in neither psql's history nor the server's log, asPASSWORD '…'in the SQL would. Make each password in your password manager from letters and digits only, so the URLs below need no percent-encoding, and paste it at the prompts. -
Let the owner's first run create the schema
The first write by a role allowed to create schema
vcicreates it. Until then reads find an empty store, and other roles getpostgres store is not set up: the first vci run with a role allowed to create schema vci creates it. Run onevci runasvci_owner, with the URL in that command's environment only. ReplaceUNITwith one of your test files, paste the line, press Enter, then pastevci_owner's password at the prompt:printf 'vci_owner password: '; read -rs pw; echo; VCI_STORE_URL="postgres://vci_owner:$pw@db.example.com/ci" vci run UNIT; unset pwread -sshows nothing of what you paste, and the shell's history keeps the line with$pwin it, not the password. Keep it to one line: when several lines are pasted at once,readtakes the next one as the password, and the password you then paste runs as a command and lands in the history. Do not add a comment to it: zsh, the default shell on macOS, reads#at the prompt as a word unlessinteractivecommentsis set. The line, and those of step 3, are for bash and zsh: fish and dash rejectread -rs, and ksh's-ssaves what you paste to its history, so in those shells runbashfirst. Then grant the other roles in databaseci, where the schema is, as the administrator (or asvci_owner, which owns it):\c ci GRANT USAGE ON SCHEMA vci TO vci_writer, vci_reader; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA vci TO vci_writer; GRANT SELECT ON ALL TABLES IN SCHEMA vci TO vci_reader; GRANT INSERT, UPDATE ON vci.memo, vci.flags TO vci_reader; -- optional: the plan's hintsA reader's
vci runrefuses each unit (postgres store refused to write: permission denied for table manifests) and keeps the tests' exit code. A job that runs untrusted code should getvci_readerwithout the optional grant, or no URL. -
Give CI the URL
For PostgreSQL the password goes in the URL, percent-encoded where it holds
@,/,?or:(letters and digits need nothing). Build each URL without showing it. Paste this line, press Enter, and pastevci_reader's password at the prompt; it sets the repository secretVCI_STORE_URL:printf 'vci_reader password: '; read -rs pw; echo; printf 'postgres://vci_reader:%s@db.example.com:5432/ci' "$pw" | gh secret set VCI_STORE_URL; unset pwThen the same with this line and
vci_writer's password, for thevci-attestenvironment's secret:printf 'vci_writer password: '; read -rs pw; echo; printf 'postgres://vci_writer:%s@db.example.com:5432/ci' "$pw" | gh secret set VCI_STORE_URL --env vci-attest; unset pwPaste one line at a time, as in step 2.
printfis built into the shell, so the password is never a program's argument. Then putVCI_STORE_URL: ${{ secrets.VCI_STORE_URL }}in each job's vci stepenv, as for Turso.VCI_STORE_TOKENis not used. -
Point laptops at it
[store] url = "postgres://vci_writer@db.example.com/ci" # a user, never a password token_command = ["op", "read", "op://Engineering/vci-postgres/password"]The command prints the password.
vci store infothen showspassword: (from token_command: …)and the URL with its user masked,postgres://***@db.example.com/ci.
Without sslmode, TLS is required, except for localhost, 127.0.0.1, ::1 and Unix sockets. Certificates are always verified, against the webpki roots and the CA file in ?sslrootcert=; a local server with a self-signed certificate needs sslrootcert or sslmode=disable. Back up with pg_dump -n vci. The full reference is in REFERENCE.md, PostgreSQL.
Set up SQLite
No server and no credential. Select it with the environment first:
export VCI_STORE=sqlite
vci store info
vci run
Before anything is stored, vci store info says status: absent: nothing is stored yet (the first `vci run` creates it), and the first vci run creates .git/vci-store.sqlite. Once everyone has 2.8.0, select it in vci.toml:
[store]
backend = "sqlite"
The file lives in the git common directory, so a clone's worktrees share it. Keep it off network file systems: SQLite's write-ahead log needs shared memory. To carry it between CI jobs, cache .git/vci-store.sqlite; each command checkpoints the log as it closes the store, so the one file holds everything. Jobs that run in parallel each save their own copy, and the last one saved wins.
For agents
The vci agent skill carries these rules for coding agents. In short: before the first vci command in a checkout (every one but vci --version, init, env, metrics and update reads the store, vci verify included), check on its own whether the checkout is reviewed. It is when it is a repository the user works on, on the default branch or the user's own branch, git diff origin/main -- :/vci.toml (the default branch in place of main; :/ is the top level from any directory) changes neither [store] url nor token_command unless the user made that change, and a script in the repository that token_command names is unchanged too. Otherwise (someone else's branch or pull request, a clone the user has not reviewed, a check that fails), put VCI_STORE=git-meta in front of every vci command until the user has reviewed it. Then: run vci store info to see which store a repository uses (its output is safe to paste); never print VCI_STORE_URL, VCI_STORE_TOKEN or what a token_command prints; never write a credential into vci.toml; in a sandbox of its own, read the store only with VCI_STORE_URL and a token for that database alone set in its environment, never through a person's com or 1Password session; never run a token_command or any other command that reads a secret, not even with its output discarded, but tell the user what failed and the command, for them to run in their own terminal; run turso db tokens create only with the user's go-ahead and piped straight into gh secret set or com secrets set --from-stdin; vci push on git-meta, and on a shared store let vci run store and leave vci ci --push to trusted CI jobs.