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
|
|
@ -347,11 +347,97 @@ namespace Crafter::Review::SeatPool {
|
|||
};
|
||||
}
|
||||
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, const std::optional<fs::path>& envFile, std::string_view specText, Ledger* ledger, const Json& apiConfig) {
|
||||
std::map<std::string, std::string> merged = env;
|
||||
if (envFile && !envFile->empty()) {
|
||||
for (const auto& [key, value] : Seats::SeatTokensFromEnvFile(Dyn::ReadText(*envFile))) merged[key] = value;
|
||||
namespace {
|
||||
// The one read of a seats file: its text, or the read's error for
|
||||
// Text() to raise where a command loads its tokens.
|
||||
void ReadOnce(SeatsFile& out, const fs::path& file) {
|
||||
out.path = file;
|
||||
try {
|
||||
out.text = Dyn::ReadText(file);
|
||||
} catch (const Dyn::Error&) {
|
||||
out.error = std::current_exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<fs::path> GlobalEnvFile() {
|
||||
fs::path dir;
|
||||
if (const std::optional<std::string> xdg = Platform::GetEnv("XDG_CONFIG_HOME"); xdg && !xdg->empty()) {
|
||||
dir = fs::path(*xdg);
|
||||
} else {
|
||||
dir = fs::path(Platform::Home()) / ".config";
|
||||
}
|
||||
const fs::path file = dir / "crafter-review" / "env";
|
||||
std::error_code error;
|
||||
if (fs::is_regular_file(file, error)) return file;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string& SeatsFile::Text() const {
|
||||
if (error) std::rethrow_exception(error);
|
||||
return text;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool IsPlacementSetting(std::string_view name) {
|
||||
return std::ranges::contains(PlacementSettings, name);
|
||||
}
|
||||
|
||||
// A seat as the seat readers count one (a token with a value), or a
|
||||
// placement setting with a value: a blank line is neither.
|
||||
bool HoldsSeatOrSetting(const std::string& text) {
|
||||
const std::map<std::string, std::string> tokens = Seats::SeatTokensFromEnvFile(text);
|
||||
if (!Seats::SeatTokensFromEnv(tokens).empty() || !Seats::ApiKeysFromEnv(tokens).empty() || !Seats::OpenRouterKeysFromEnv(tokens).empty()) return true;
|
||||
return std::ranges::any_of(Seats::SettingsFromEnvFile(text), [](const auto& entry) { return IsPlacementSetting(entry.first) && !Dyn::Strip(entry.second).empty(); });
|
||||
}
|
||||
}
|
||||
|
||||
SeatsFile ResolveSeatsFile(const std::optional<std::string>& given, const fs::path& cwd) {
|
||||
SeatsFile out;
|
||||
if (given) {
|
||||
if (!given->empty()) ReadOnce(out, *given);
|
||||
return out;
|
||||
}
|
||||
if (const std::optional<std::string> named = Platform::GetEnv("SEATS_ENV_FILE"); named && !named->empty()) {
|
||||
ReadOnce(out, *named);
|
||||
return out;
|
||||
}
|
||||
// The working directory's .env is the seats file when it is a file
|
||||
// holding a seat or a placement setting, each with a value (a blank
|
||||
// placeholder line is neither), or one that cannot be read as UTF-8
|
||||
// text (its error is then the command's to report). A
|
||||
// repository's application config (its Vite or Supabase lines), or a
|
||||
// .env that is no file (a Python venv), is passed over for the
|
||||
// machine's own file (2026-11-01, the self-review's rounds 9 and 10).
|
||||
std::error_code error;
|
||||
if (fs::exists(cwd / ".env", error)) {
|
||||
if (fs::is_regular_file(cwd / ".env", error)) {
|
||||
ReadOnce(out, cwd / ".env");
|
||||
if (out.error || HoldsSeatOrSetting(out.text)) return out;
|
||||
out = SeatsFile{};
|
||||
}
|
||||
out.passedOver = cwd / ".env";
|
||||
}
|
||||
if (const std::optional<fs::path> global = GlobalEnvFile()) ReadOnce(out, *global);
|
||||
return out;
|
||||
}
|
||||
|
||||
Settings SettingsOf(const SeatsFile& file) {
|
||||
Settings out {Platform::Environment(), {}, {}};
|
||||
for (const auto& [name, value] : Seats::SettingsFromEnvFile(file.text)) {
|
||||
if (!IsPlacementSetting(name)) out.ignored.push_back(name);
|
||||
else if (!Dyn::Strip(value).empty() && out.env.try_emplace(name, value).second) out.fromFile.push_back(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> WithFileTokens(std::map<std::string, std::string> env, const SeatsFile& file) {
|
||||
for (const auto& [key, value] : Seats::SeatTokensFromEnvFile(file.Text())) env[key] = value;
|
||||
return env;
|
||||
}
|
||||
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view specText, Ledger* ledger, const Json& apiConfig) {
|
||||
const std::map<std::string, std::string>& merged = env;
|
||||
std::vector<std::pair<std::string, Json>> pool;
|
||||
for (const auto& [n, key] : Seats::ApiKeysFromEnv(merged)) pool.emplace_back(std::format("api{}", n), NewSeat(std::format("api{}", n), "api", key));
|
||||
for (const auto& [n, token] : Seats::SeatTokensFromEnv(merged)) pool.emplace_back(std::to_string(n), NewSeat(std::to_string(n), "oauth", token));
|
||||
|
|
|
|||
Loading…
Reference in a new issue