feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
This commit is contained in:
parent
0e0983f278
commit
e215ad986e
16 changed files with 483 additions and 91 deletions
|
|
@ -71,7 +71,11 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
|
|||
const bool seat = k.starts_with("CLAUDE_CODE_OAUTH_TOKEN") || k.starts_with("ANTHROPIC_API_KEY") || k.starts_with("OPENROUTER_KEY");
|
||||
if (!review && !seat) Env[k] = v;
|
||||
}
|
||||
Env = With(Env, {{"GIT_CONFIG_GLOBAL", "/dev/null"}, {"GIT_CONFIG_SYSTEM", "/dev/null"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
|
||||
// No seats file of the machine's own: XDG_CONFIG_HOME natively, and
|
||||
// HOST_XDG_CONFIG_HOME, which the Flatpak's Platform::Init copies
|
||||
// over it - a directory of this run's own, which nothing creates.
|
||||
const std::string noConfig = (base / "no-config").string();
|
||||
Env = With(Env, {{"XDG_CONFIG_HOME", noConfig}, {"HOST_XDG_CONFIG_HOME", noConfig}, {"GIT_CONFIG_GLOBAL", "/dev/null"}, {"GIT_CONFIG_SYSTEM", "/dev/null"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
|
||||
{"REVIEW_GATES_BOARD", (base / "board.json").string()}, {"REVIEW_GATES_SCRATCH", (base / "scratch").string()}, {"TZ", "UTC"}});
|
||||
auto repo = [&](std::string_view name) {
|
||||
const fs::path dir = base / name;
|
||||
|
|
@ -196,6 +200,22 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
|
|||
Check("stop --hash: every live run of the hash on the board gets its stop file (the first 8 characters decide); another hash finds none (exit 1); the board is read, never written",
|
||||
byHash.returnCode == 0 && wroteHash && otherHash.returnCode == 1 && otherHash.err.contains("no live run of 00000000 on the board") && Read(base / "board.json") == boardBefore,
|
||||
byHash.out + byHash.err + otherHash.err);
|
||||
// Only `run` and `seats` read the seats file, and its lines reach the
|
||||
// placement readers alone (rounds 8 and 11 of the self-review): a
|
||||
// `stop` with the board path only in the working directory's .env
|
||||
// looks at the machine's default board, as every driver does - under
|
||||
// a HOME of the test's own here.
|
||||
std::map<std::string, std::string> envNoPaths = Env;
|
||||
envNoPaths.erase("REVIEW_GATES_BOARD");
|
||||
envNoPaths.erase("REVIEW_GATES_SCRATCH");
|
||||
envNoPaths["HOME"] = (base / "home").string();
|
||||
const fs::path fileRepo = base / "file-repo";
|
||||
fs::create_directories(fileRepo);
|
||||
Write(fileRepo / ".env", std::format("REVIEW_GATES_BOARD={}\nREVIEW_GATES_SCRATCH={}\n", (base / "file-board.json").string(), (base / "file-scratch").string()));
|
||||
const Process::Captured fromFile = Process::Run({Platform::SelfExe().string(), "stop", "--hash", "00000000"}, fileRepo, std::chrono::seconds(60), {}, envNoPaths);
|
||||
Check("no line of the seats file reaches `stop`: with the board's and the scratch root's paths only in the working directory's .env it reads the machine's default board", fromFile.returnCode == 1
|
||||
&& fromFile.err.contains(std::format("on the board ({})", (base / "home" / ".cache" / "review-gates" / "seat-board.json").string())) && !fromFile.err.contains("file-board"),
|
||||
fromFile.err);
|
||||
|
||||
// seats on the stand-in: a token from the env file, an API key, the
|
||||
// CLI's own login.
|
||||
|
|
@ -207,15 +227,41 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
|
|||
const std::map<std::string, std::string> withFake = {{"CLAUDE_BIN", fake.string()}};
|
||||
const Process::Captured report = Self({"seats", "--local", "--env-file", ".env"}, base / "seats", withFake);
|
||||
const Process::Captured json = Self({"seats", "--json", "--no-fable"}, base / "seats", withFake);
|
||||
// --env (a prefix of --env-file) names the file for the tokens and
|
||||
// the settings alike, with ./.env in the way (the API seat's section
|
||||
// is where the order shows).
|
||||
Write(base / "seats" / "other.env", "CLAUDE_CODE_OAUTH_TOKEN2=tok-two\nANTHROPIC_API_KEY2=sk-ant-api-two\nREVIEW_GATES_SEAT_ORDER=oauth-first\n");
|
||||
const Process::Captured other = Self({"seats", "--no-fable", "--env", "other.env"}, base / "seats", withFake);
|
||||
// An application's .env (no seat line, no setting) is passed over for
|
||||
// the machine's own seats file.
|
||||
Write(base / "app" / ".env", "VITE_API_URL=http://localhost:54321\n");
|
||||
Write(base / "app-xdg" / "crafter-review" / "env", "CLAUDE_CODE_OAUTH_TOKEN3=tok-three\n");
|
||||
const Process::Captured appSeats =
|
||||
Self({"seats", "--no-fable"}, base / "app", {{"CLAUDE_BIN", fake.string()}, {"XDG_CONFIG_HOME", (base / "app-xdg").string()}, {"HOST_XDG_CONFIG_HOME", (base / "app-xdg").string()}});
|
||||
// A seats file that can be read only once - a FIFO, as `--env-file
|
||||
// <(gpg -d seats.env.gpg)` gives one - is read once: its settings and
|
||||
// its tokens come from that one read (round 10 of the self-review).
|
||||
fs::create_directories(base / "fifo");
|
||||
const Process::Captured fromFifo = Process::Run(
|
||||
{"sh", "-c",
|
||||
std::format("mkfifo seats.fifo && (printf 'CLAUDE_CODE_OAUTH_TOKEN4=tok-four\\nANTHROPIC_API_KEY4=sk-ant-api-four\\nREVIEW_GATES_SEAT_ORDER=oauth-first\\n' > seats.fifo &) && exec '{}' seats --no-fable --env-file seats.fifo",
|
||||
Platform::SelfExe().string())},
|
||||
base / "fifo", std::chrono::seconds(60), {}, With(Env, withFake));
|
||||
Json parsed;
|
||||
try {
|
||||
parsed = Json::parse(json.out);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
Check("seats: every seat probed - the CLI's own login, the env file's token, the API key - its windows read, the API seat beside the driver's ledger, the drivers on the board",
|
||||
Check("seats: every seat probed - the CLI's own login, the env file's token, the API key - its windows read, the API seat beside the driver's ledger, the drivers on the board; an application's .env "
|
||||
"is passed over for the machine's own file; a FIFO is read once, its settings and its seats alike",
|
||||
report.returnCode == 0 && report.out.starts_with("seat pool: 3 seat(s) (local, 1, api1) from .env and your own login, probed ")
|
||||
&& report.out.contains("| local | 42% |") && report.out.contains("| 1 | 42% |") && report.out.contains("| api1 | ok | $0.00 of $200") && report.out.contains("drivers: none running"),
|
||||
report.out + report.err);
|
||||
&& report.out.contains("| local | 42% |") && report.out.contains("| 1 | 42% |") && report.out.contains("| api1 | ok | $0.00 of $200") && report.out.contains("drivers: none running")
|
||||
&& other.returnCode == 0 && other.out.starts_with("seat pool: 2 seat(s) (2, api2) from other.env, probed ") && other.out.contains("| api2 | ok | $0.00 of $200")
|
||||
&& other.out.contains("oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first)") && !other.out.contains("tok-two") && !other.out.contains("sk-ant-api-two")
|
||||
&& appSeats.returnCode == 0 && appSeats.out.starts_with(std::format("seat pool: 1 seat(s) (3) from {}, probed ", (base / "app-xdg" / "crafter-review" / "env").string()))
|
||||
&& fromFifo.returnCode == 0 && fromFifo.out.starts_with("seat pool: 2 seat(s) (4, api4) from seats.fifo, probed ")
|
||||
&& fromFifo.out.contains("oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first)"),
|
||||
report.out + report.err + other.out + other.err + appSeats.out + appSeats.err + fromFifo.out + fromFifo.err);
|
||||
Check("seats --json: the probe results as JSON - the seats, the API seats, the eligible ones and the next placement; ./.env read by default; no token, no key printed",
|
||||
json.returnCode == 0 && parsed.is_object() && parsed.at("seats").size() == 1 && parsed.at("seats").at(0).at("probedWith") == "haiku" && parsed.at("apiSeats").size() == 1
|
||||
&& parsed.at("eligible") == Json::array({1}) && parsed.at("next").at("seat") == 1 && Dyn::Str(parsed.at("probedAt")).ends_with("Z")
|
||||
|
|
|
|||
Loading…
Reference in a new issue