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
|
|
@ -199,9 +199,9 @@ export namespace Crafter::Review::Run {
|
|||
// {lens file: environment}, in the config's lens order: the dump, the
|
||||
// resume file and the scratch dir's marker write it in that order.
|
||||
Json environments = Json::object();
|
||||
// The working directory: the config, the env file and the slots are
|
||||
// its, the lens processes run in it, and every path logged is relative
|
||||
// to it.
|
||||
// The working directory: the config and the slots are its, the seats
|
||||
// file is looked up from it (SeatPool::ResolveSeatsFile), the lens
|
||||
// processes run in it, and every path logged is relative to it.
|
||||
std::filesystem::path repo;
|
||||
// The repository under review, from the artifact's freeze record (the
|
||||
// working directory when it has none): the prompt names it, and the
|
||||
|
|
@ -278,6 +278,8 @@ export namespace Crafter::Review::Run {
|
|||
std::unique_ptr<Memory::Sampler> sampler;
|
||||
std::unique_ptr<Pool::Runner> runner;
|
||||
Json apiCfg = Json::object();
|
||||
// The placement settings Gate 0 laid out (SeatPool::SettingsOf).
|
||||
std::map<std::string, std::string> settingsEnv;
|
||||
std::unique_ptr<SeatPool::Ledger> ledger;
|
||||
std::vector<Json> seats;
|
||||
std::map<std::filesystem::path, Json> envMarkers;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,67 @@ export namespace Crafter::Review::SeatPool {
|
|||
bool IsPaid(const Json& seat);
|
||||
// The API-seat knobs.
|
||||
Json ApiConfig(const std::map<std::string, std::string>& env);
|
||||
// The seats file of `run` or `seats` (2026-11-01): the --env-file given
|
||||
// (an empty one: none), else $SEATS_ENV_FILE, else <cwd>/.env - the
|
||||
// working directory's, the operator's, not the repository under
|
||||
// review's - when it is a file holding a seat or a placement setting,
|
||||
// each with a value (an application's .env, one of blank lines, or one
|
||||
// that is no file, is passed over and recorded so), else the machine's
|
||||
// own (GlobalEnvFile); none: this
|
||||
// machine's own login. Each file is read once, and the command takes
|
||||
// its settings and its tokens from that read - the tokens through
|
||||
// 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 (the self-review's round 10: read twice, it lost its seats or
|
||||
// hung).
|
||||
struct SeatsFile {
|
||||
std::optional<std::filesystem::path> path;
|
||||
std::optional<std::filesystem::path> passedOver;
|
||||
std::string text;
|
||||
std::exception_ptr error;
|
||||
// The text as read; the read's own error, raised again, when the
|
||||
// read failed (a file missing or not UTF-8 ends the command).
|
||||
const std::string& Text() const;
|
||||
};
|
||||
// The lookup and its one read.
|
||||
SeatsFile ResolveSeatsFile(const std::optional<std::string>& given, const std::filesystem::path& cwd);
|
||||
// The settings a seats file may carry: every name ApiConfig, PoolConfig
|
||||
// and Seats::ConfigFromEnv read (2026-11-01, the self-review's round 19:
|
||||
// one list for the map they take, its log and what makes a working
|
||||
// directory's .env a seats file).
|
||||
constexpr std::array<std::string_view, 15> PlacementSettings = {
|
||||
"REVIEW_GATES_API_BUDGET_USD", "REVIEW_GATES_API_LENS_COST_USD", "REVIEW_GATES_API_CONCURRENCY", "REVIEW_GATES_API_CYCLE_DAY",
|
||||
"REVIEW_GATES_SEAT_ORDER", "REVIEW_GATES_OPENROUTER_BUDGET_USD", "REVIEW_GATES_OPENROUTER_LENS_COST_USD",
|
||||
"REVIEW_GATES_OPENROUTER_CONCURRENCY", "REVIEW_GATES_LENS_COST_PCT", "REVIEW_GATES_LENS_COST_7D_PCT",
|
||||
"REVIEW_GATES_OAUTH_CONCURRENCY", "REVIEW_GATES_SEAT_MAX_UTIL", "REVIEW_GATES_SEAT_WAIT_MIN", "REVIEW_GATES_SEAT_HANDOVER_PCT",
|
||||
"REVIEW_GATES_SEAT_POLICY"};
|
||||
// The settings the placement readers take (ApiConfig, PoolConfig,
|
||||
// Seats::ConfigFromEnv): the process environment, and the file's
|
||||
// PlacementSettings lines where it has no such variable - the Python
|
||||
// driver's config_env (2026-10-17: a setting lives beside the seats it
|
||||
// orders). A map for those readers alone: the process environment is
|
||||
// not touched, so no other reader sees a line of the file, and a path's
|
||||
// or a binary's line reaches nothing (the self-review's round 11). The
|
||||
// names the file supplied are `fromFile`; its REVIEW_GATES_* lines of
|
||||
// any other name, which only the process environment sets, are
|
||||
// `ignored` - named by the run and `seats`, never taken in silence.
|
||||
struct Settings {
|
||||
std::map<std::string, std::string> env;
|
||||
std::vector<std::string> fromFile;
|
||||
std::vector<std::string> ignored;
|
||||
};
|
||||
Settings SettingsOf(const SeatsFile& file);
|
||||
// env with the seats file's seat lines over it, from the file's one read
|
||||
// (Text(): a read that failed raises its error here) - the one place a
|
||||
// command lays the file's tokens over an environment (the self-review's
|
||||
// round 19: run and seats had each done it by hand, and LoadSeats read
|
||||
// the file a second time by path).
|
||||
std::map<std::string, std::string> WithFileTokens(std::map<std::string, std::string> env, const SeatsFile& file);
|
||||
// The machine's own seats file, read when a run or `seats` has no
|
||||
// --env-file, no $SEATS_ENV_FILE and no seats ./.env: $XDG_CONFIG_HOME/crafter-review/env,
|
||||
// else ~/.config/crafter-review/env - when it exists (2026-11-01, one
|
||||
// file for every repository on a machine; a repository's own .env wins).
|
||||
std::optional<std::filesystem::path> GlobalEnvFile();
|
||||
// The other drivers' load on a seat as the last board fold left it.
|
||||
Json Foreign(const Json& seat);
|
||||
std::filesystem::path LedgerPath(const std::optional<std::filesystem::path>& scratchRoot = std::nullopt);
|
||||
|
|
@ -97,13 +158,13 @@ export namespace Crafter::Review::SeatPool {
|
|||
};
|
||||
|
||||
Json NewSeat(const std::string& id, const std::string& kind, const std::string& secret);
|
||||
// The seats a run may use. spec: auto (every Anthropic seat of the env
|
||||
// file and the environment, else local), local | api | oauth | openrouter,
|
||||
// The seats a run may use. spec: auto (every Anthropic seat of env - the
|
||||
// seats file's over the process environment's, WithFileTokens - else
|
||||
// local), local | api | oauth | openrouter,
|
||||
// or a list such as api1,1,2,local. std::invalid_argument with a
|
||||
// ValueError message for a seat that is not configured, or OpenRouter
|
||||
// mixed with the rest.
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, const std::optional<std::filesystem::path>& envFile, std::string_view spec,
|
||||
Ledger* ledger = nullptr, const Json& apiConfig = nullptr);
|
||||
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view spec, Ledger* ledger = nullptr, const Json& apiConfig = nullptr);
|
||||
using ProbeFunction = std::function<Json(std::int32_t seat, const std::string& token, const std::string& model, double timeout, const std::string& claudeBin, bool local, bool api)>;
|
||||
// One probe per seat before the run; a seat already out is skipped.
|
||||
void Probe(std::vector<Json>& seats, const std::string& claudeBin, double timeout, const std::vector<std::string>& models,
|
||||
|
|
|
|||
|
|
@ -47,9 +47,19 @@ export namespace Crafter::Review::Seats {
|
|||
std::vector<std::pair<std::int32_t, std::string>> SeatTokensFromEnv(const std::map<std::string, std::string>& env);
|
||||
std::vector<std::pair<std::int32_t, std::string>> ApiKeysFromEnv(const std::map<std::string, std::string>& env);
|
||||
std::vector<std::pair<std::int32_t, std::string>> OpenRouterKeysFromEnv(const std::map<std::string, std::string>& env);
|
||||
// Only the seat lines of a dotenv file; `export` and quotes stripped,
|
||||
// everything else ignored.
|
||||
// Every `[export ]NAME=value` line of a dotenv text, in order: blanks
|
||||
// around the `=` and the value dropped, one pair of matching quotes
|
||||
// stripped; anything else skipped.
|
||||
std::vector<std::pair<std::string, std::string>> DotenvLines(std::string_view text);
|
||||
// Only the seat lines of those: the OAuth tokens, the numbered API keys
|
||||
// and the OpenRouter keys; everything else ignored.
|
||||
std::map<std::string, std::string> SeatTokensFromEnvFile(std::string_view text);
|
||||
// The REVIEW_GATES_* lines of a dotenv file (quotes and `export`
|
||||
// stripped). SeatPool::SettingsOf lays the placement settings among them
|
||||
// (SeatPool::PlacementSettings) under the process environment in the map
|
||||
// the placement readers take, where no other reader sees them, and names
|
||||
// the rest.
|
||||
std::map<std::string, std::string> SettingsFromEnvFile(std::string_view text);
|
||||
|
||||
// The CLI's fraction as a percent, past 100 for a seat over its limit.
|
||||
Json Pct(const Json& utilization);
|
||||
|
|
|
|||
Loading…
Reference in a new issue