Skip to main content

Module pool

Module pool 

Source
Expand description

The key-pool state file: pools.json.

A key pool is several interchangeable credentials for one service. Which one to hand out next, which are cooling after a rate limit, and how often each has been used are per-machine facts, so they are kept here rather than in the vault. Three reasons, worst failure first:

  1. save_vault appends a vault_audit row on every update. A CI loop calling unv exec would grow the hash-chained log without bound - the same reason read events stopped being audited.
  2. save_vault is a compare-and-swap. Concurrent reads against one vault would collide and start returning conflicts for reads.
  3. A vault is shared; a rotation cursor is not. Two CI runners pulling from one remote vault want independent cursors.

§Why this lives in vault-core

Two programs read this file: unv and the desktop app. The app’s app_data_dir and the CLI’s dirs::data_dir()/io.unenverse resolve to the same directory, so one vault produces the same local_vault_key in both - report a key rate limited from CI and the desktop shows it cooling.

That only holds while both agree on the file’s shape, its path, and the cooldown arithmetic. This project has been bitten by two implementations of one format drifting apart often enough to have a convention about it, so there is exactly one implementation and both callers use it.

§Shape

{
  "version": 1,
  "vaults": {
    "local:/home/me/.local/share/io.unenverse/vault.db": {
      "github-ci": {
        "cursor": 1,
        "members": {
          "<entry_ck>": {
            "uses": 12,
            "last_used_at": "2026-08-25T11:02:31Z",
            "cooling_until": "2026-08-25T11:17:31Z"
          }
        }
      }
    }
  }
}

Members are keyed by entry_ck - the same stable identity version history and audit attribution use - so renaming an entry keeps its pool state, and two entries sharing a provider never collapse into one.

Structs§

MemberState
One member’s stored state.

Functions§

cursor
The cursor for a pool, or 0.
forget
Forget a pool’s cursor, cooldowns and counts for one vault.
is_cooling
Is this member cooling at now?
iso_at
RFC 3339 for an arbitrary instant.
load
Read the state file, or an empty document.
local_vault_key
State is filed per vault, so connecting to a different server does not inherit the last one’s cursor - the same reasoning as resetting view state on a vault switch in the app.
member_state
Read one member’s state out of a loaded document.
now_ts
Seconds since the epoch, now.
parse_rfc3339
Read one of the timestamps iso_at writes: exactly YYYY-MM-DDTHH:MM:SSZ.
pick_index
Round-robin over the members that are not cooling, starting at cursor.
record_use
Record that a member was handed out, and where the cursor moves to.
remote_vault_key
The remote counterpart of local_vault_key.
save
Write the state file, creating its directory and restricting it to its owner.
set_cooldown
Put a member on cooldown until until_ts, or clear its cooldown with None.
state_path
Where pools.json lives.