okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
8fc7762
main
usage feed
provider usage and quota, through provide-usage: a pure Zig core (usage-core) that knows each provider's rules, and an albedo driver that owns every byte of I/O. the daemon never grows provider-specific quota code; a new provider is a usage-core bump plus the pinned flake input moving.
the split
usage-core never opens a socket, reads a clock, or touches the filesystem. a provider is a state machine: given a credential and the responses so far, usage advance says either which requests to send next or the finished report. albedo sends those requests, hands the bodies back, and repeats. one feed is a handful of round trips at most.
the driver is two files:
src/albedo/harness/usage_feed.gleam— the loop, the envelope, and the report types.fetch(provider, credential, now_ms)runs a feed to completion. a provider failure (a 4xx, an unknown provider, a flakybl) is data:Ok(Report(error: Some(..))).Errormeans the driver itself failed: the binary is missing, its output is unparseable, or the feed did not finish in 6 rounds.src/albedo/harness/albedo_usage_core.erl— the I/O half: oneusage advance -process per round,httprequests throughalbedo_http, andcommandexecution.
each round is a fresh process fed one JSON envelope line on stdin: {"provider", "credential", "responses": [round, ...], "nowMs"}. the envelope carries the credential, so it never reaches argv where other local processes could read it. the host keeps the state (it resends every round's responses) and the core keeps the rules; neither holds anything the other could corrupt. an erlang port cannot half-close stdin, and the CLI stops reading at the first newline, so the driver writes the line and reads stdout to exit: one step line per replayed round, the last line is the answer.
the binary resolves like albedo-render: priv/bin/usage first, then PATH. ALBEDO_USAGE_CORE overrides both — tests point it at a fake, and a value that does not exist is an error, never a silent fallback.
requests and responses
a step asks for requests; each is answered with one {"status", "body"} object:
http— sent through albedo's one httpc front door, verified TLS and all. a transport failure is data: status0with the reason in the body, so a feed reports it instead of the call failing.command(alibaba'sbl) — run with onlyPATH,HOME,LANG,LC_ALL,LC_CTYPE,TZand the proxy variables in the environment, a timeout (the request'stimeoutMs, 15 s by default), and a 64 KiB stdout cap.127means "this host will not run commands" and is reserved; a command that cannot be found or timed out answers126, and the feed routes around either the same way.
packaging
The driver's timeout cleanup requires a kill executable on PATH. It sends KILL to the child process because closing the Erlang port alone does not guarantee that the child exits.
usage-core is a pinned tarball flake input (flake = false) on the knot, built in default.nix with nixpkgs' zig (its build.zig.zon demands 0.16.0, which nixpkgs-unstable carries). the CLI is linked into the daemon next to albedo-render:
nix build .#albedo.usageCore # the derivation alone nix build .#daemon # the whole daemon, priv/bin/usage included
consumers
the daemon-side poller (per-account quota) reads Report and Limit and records what arrives; nothing else should parse the core's JSON. Limit.resets_at is epoch milliseconds; used_percent is clamped 0–100; absent optionals mean the provider did not state the value, never "zero".
1# usage feed 2 3provider usage and quota, through [provide-usage](https://next.tangled.org/ptr.pet/provide-usage): a pure Zig core (`usage-core`) that knows each provider's rules, and an albedo driver that owns every byte of I/O. the daemon never grows provider-specific quota code; a new provider is a `usage-core` bump plus the pinned flake input moving. 4 5## the split 6 7`usage-core` never opens a socket, reads a clock, or touches the filesystem. a provider is a state machine: given a credential and the responses so far, `usage advance` says either which requests to send next or the finished report. albedo sends those requests, hands the bodies back, and repeats. one feed is a handful of round trips at most. 8 9the driver is two files: 10 11- [`src/albedo/harness/usage_feed.gleam`](../src/albedo/harness/usage_feed.gleam) — the loop, the envelope, and the report types. `fetch(provider, credential, now_ms)` runs a feed to completion. a provider failure (a 4xx, an unknown provider, a flaky `bl`) is data: `Ok(Report(error: Some(..)))`. `Error` means the driver itself failed: the binary is missing, its output is unparseable, or the feed did not finish in 6 rounds. 12- [`src/albedo/harness/albedo_usage_core.erl`](../src/albedo/harness/albedo_usage_core.erl) — the I/O half: one `usage advance -` process per round, `http` requests through [`albedo_http`](extensions.md), and `command` execution. 13 14each round is a fresh process fed one JSON envelope line on stdin: `{"provider", "credential", "responses": [round, ...], "nowMs"}`. the envelope carries the credential, so it never reaches argv where other local processes could read it. the host keeps the state (it resends every round's responses) and the core keeps the rules; neither holds anything the other could corrupt. an erlang port cannot half-close stdin, and the CLI stops reading at the first newline, so the driver writes the line and reads stdout to exit: one step line per replayed round, the last line is the answer. 15 16the binary resolves like `albedo-render`: `priv/bin/usage` first, then `PATH`. `ALBEDO_USAGE_CORE` overrides both — tests point it at a fake, and a value that does not exist is an error, never a silent fallback. 17 18## requests and responses 19 20a step asks for requests; each is answered with one `{"status", "body"}` object: 21 22- `http` — sent through albedo's one httpc front door, verified TLS and all. a transport failure is data: status `0` with the reason in the body, so a feed reports it instead of the call failing. 23- `command` (alibaba's `bl`) — run with only `PATH`, `HOME`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TZ` and the proxy variables in the environment, a timeout (the request's `timeoutMs`, 15 s by default), and a 64 KiB stdout cap. `127` means "this host will not run commands" and is reserved; a command that cannot be found or timed out answers `126`, and the feed routes around either the same way. 24 25## packaging 26 27The driver's timeout cleanup requires a `kill` executable on `PATH`. It sends `KILL` to the child process because closing the Erlang port alone does not guarantee that the child exits. 28 29`usage-core` is a pinned tarball flake input (`flake = false`) on the knot, built in `default.nix` with nixpkgs' zig (its `build.zig.zon` demands 0.16.0, which nixpkgs-unstable carries). the CLI is linked into the daemon next to `albedo-render`: 30 31``` 32nix build .#albedo.usageCore # the derivation alone 33nix build .#daemon # the whole daemon, priv/bin/usage included 34``` 35 36## consumers 37 38the daemon-side poller (per-account quota) reads `Report` and `Limit` and records what arrives; nothing else should parse the core's JSON. `Limit.resets_at` is epoch milliseconds; `used_percent` is clamped 0–100; absent optionals mean the provider did not state the value, never "zero".