okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffix(usage): resolve kill from PATH for timeout cleanup8fc7762

main
3.7 KiB38 linesraw

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 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.
  • src/albedo/harness/albedo_usage_core.erl — the I/O half: one usage advance - process per round, http requests through albedo_http, and command execution.

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: status 0 with the reason in the body, so a feed reports it instead of the call failing.
  • 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.

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".