yog 0.0.51

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! brazen's effective provider table, projected (DESIGN §5.1 #20/#21, §8.3).
//!
//! One row per provider `bz` would route to, read from `bz --list-providers
//! --json` — the **linked** brazen's own serde projection (§16.7 W10), not a
//! scan of the config text. yog re-implements none of it: the `name` column
//! names the row, the `auth` column names its credential model, the `protocol`
//! column names its wire dialect, and the `credential` column names the secret
//! a run would actually reach for — all four spelled by brazen's own serde
//! renames.
//!
//! **What the tool capability means for a yog turn lives beside it** in
//! [`capability`]: the refusal itself (bl-3d22) and the dead step's route back
//! to it (bl-5252). Since bl-b6c9 the JUDGEMENT is not there either — it is
//! brazen's [`tools`](ProviderRow::tools) column (upstream bl-5053), so a
//! dialect this build was never compiled against is judged by the crate that
//! owns the encoder. This module owns the columns; that one owns the sentence a
//! refusal is worded in.
//!
//! **`auth` is the login capability.** brazen's `Provider::oauth` is documented
//! `present exactly when auth = "oauth2"` — resolution pairs the two or fails
//! (→78), so `auth == "oauth2"` *is* "this row has an `oauth` block", answered by
//! the crate that owns the invariant. `bz --login` serves oauth rows only, so
//! every other spelling is a row it can only refuse. That is the whole of the
//! capability question the Login surface asks (§8.3), which is why nothing here
//! reclassifies a row: it reads a column.
//!
//! **The rendered row is derived here too** ([`ProviderRowView`], bl-402f), not
//! in the pane: `auth` plus the §5.1 #22 presence read is the whole of what a
//! surface can say about a provider, so the words that say it live beside the
//! columns they read. The §8.3 Login pane is the one **painted** seat at it
//! (bl-20cb retired the §9.5 config copy); the boundary's `Providers` reply and
//! §9.4's remedy sentence read the same derivation without repainting the row.
//!
//! **`device` is the flow capability.** The column names the headless sign-in a
//! row serves, in brazen's own `device.style` spelling. `authorize_url`/
//! `token_url` are required fields of every `oauth` block while `device` is
//! optional, so the **browser** flow stays the floor every oauth row can serve
//! and this column says which rows do better: [`ProviderRow::headless_login`] is
//! the whole of the branch, read once at the spawn (§8.3 rule 1 as amended by
//! bl-61bf; [`crate::login`]).

/// brazen's `auth` spelling for an OAuth row (`AuthId::OAuth2`'s serde rename).
const OAUTH2: &str = "oauth2";

/// The keyless spelling: the row needs no credential at all (a local runtime,
/// or a host CLI that carries its own).
const NONE: &str = "none";

/// The two keyed spellings: the secret is a *config* value, not a bz-stored
/// credential, so the operator's move is an edit and never a sign-in.
const API_KEY: &str = "api_key";
const BEARER: &str = "bearer";

/// brazen's `credential` spelling for a row that needs none at all — its
/// `auth = "none"` arm, answered before any store is consulted. A keyless row
/// is a local runtime or a host CLI carrying its own sign-in, so this says
/// nothing about whether that runtime is *there* — see the §8.1 sign-in gate
/// (`boundary::dispatch::signin`), the one consumer that cares.
pub const NOT_REQUIRED: &str = "not required";

/// brazen's `credential` spelling for a row with no credential to use: its
/// `fetch_cred` found nothing. **The one spelling that is a refusal** — every
/// other, this build's four known ones and any this build has never heard of,
/// leaves the row able to answer, and no surface refuses on the strength of a
/// question that went unanswered.
pub const MISSING: &str = "missing";

/// One row of the effective provider table (§5.1 #20/#21) — the columns yog
/// consumes, verbatim from brazen's `--list-providers --json` object.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderRow {
    /// The row's name — the `--provider <name>` selector and the credential
    /// file's stem.
    pub name: String,
    /// The wire dialect the row speaks, in brazen's own `ProtocolId` spelling
    /// (`anthropic_messages`, `claude_code`, …) — the column
    /// [`capability`]'s two reads judge.
    pub protocol: String,
    /// brazen's credential model for the row: `oauth2` / `api_key` / `bearer` /
    /// `none`.
    pub auth: String,
    /// **The credential this row would actually use**, in brazen's own
    /// spelling: `stored` (a credential bz owns), `ambient` (one it found
    /// outside its store), `inline` (a key written into the config), `not
    /// required` ([`NOT_REQUIRED`] — the keyless arm) or `missing`
    /// ([`MISSING`]). brazen computes it through the very `fetch_cred` a run
    /// spends, minus the network, so it is the authority on "could this row
    /// answer at all" and yog re-derives none of it. Read by the §8.1 sign-in
    /// gate at the `Prompt` door (`boundary::dispatch::signin`, bl-1fd0 seated
    /// by bl-2291).
    pub credential: String,
    /// **Does this row take an `effort:` assignment?** (§9.4's tuning pair,
    /// bl-23bd.) brazen's own per-row declaration, read as a column — the
    /// dialect's `Protocol::tuning()` answer AND this row's own
    /// `unsupported_body_keys` decline, folded by the crate that owns both.
    pub effort: bool,
    /// **Does this row take a `priority:` assignment?** The same column pair,
    /// over the `service_tier` wire spelling: the OpenAI family and Anthropic
    /// have one, and the others narrow it away.
    pub priority: bool,
    /// **Can this row's dialect carry a tool declaration at all?** (§9.4,
    /// bl-b6c9.) brazen's own `Protocol::shapes()` answer, published as a
    /// column since 0.0.12 (upstream bl-5053) and proved there against each
    /// dialect's own `encode` — the fact [`capability`] used to re-derive from
    /// a total match on `ProtocolId`, which by construction could only judge
    /// the dialects THIS build was compiled against.
    ///
    /// `None` is **no answer**, not a refusal: a `bz` older than 0.0.12 serves
    /// no such key, and no surface may refuse on the strength of a question
    /// that went unanswered ([`is_unknown_row`](crate::model_pick::grammar::is_unknown_row)'s
    /// discipline). That is why it is an `Option<bool>` where
    /// [`effort`](Self::effort) and [`priority`](Self::priority) are plain
    /// bools: those two gate a control, this one gates a WRITE.
    pub tools: Option<bool>,
    /// **Which headless sign-in this row serves**, in brazen's own
    /// `device.style` spelling (`rfc8628`, `codex`) — empty for a row that
    /// serves none. A *style* rather than a boolean because the endpoint's
    /// grammar is the row's fact too and brazen selects the wire from it; yog's
    /// one reader ([`headless_login`](Self::headless_login)) asks only whether
    /// there is one.
    pub device: String,
}

impl ProviderRow {
    /// Why `bz --login` cannot serve this row, or `None` when it can (§8.3).
    /// The Login surface renders the button exactly when this is `None`, and
    /// **this sentence instead of a button** otherwise (bl-402f): a row that
    /// could only exit 78 gets no verb at all, and the sentence is the
    /// operator's actual next move, per credential model. An `auth` spelling
    /// this build does not know is quoted rather than guessed at.
    pub fn login_blocked(&self) -> Option<String> {
        match self.auth.as_str() {
            OAUTH2 => None,
            NONE => Some("keyless — nothing to log in".to_owned()),
            API_KEY => Some("api-key provider — set the key in Config".to_owned()),
            BEARER => Some("bearer-token provider — set the token in Config".to_owned()),
            other => Some(format!(
                "auth \"{other}\" — bz --login signs in oauth2 rows only"
            )),
        }
    }

    /// **Can this row be signed in with no browser on the engine's box?** (§8.3
    /// rule 1 as amended by bl-61bf.) The [`device`](Self::device) column alone:
    /// a row declaring a device endpoint serves the seat-independent flow — bz
    /// binds nothing and streams the verification URL and user code to whichever
    /// seat asked — while a row declaring none can only serve the loopback
    /// browser flow, which completes where the browser reaches the engine.
    ///
    /// **One column, deliberately not two.** It does not re-ask
    /// [`login_blocked`](Self::login_blocked): brazen carries `device` inside the
    /// `oauth` block, so a row with no login capability has no column to declare
    /// it with, and the two answer separate questions — whether the verb is
    /// offered, and which flow it fires.
    pub fn headless_login(&self) -> bool {
        !self.device.is_empty()
    }

    /// Does this row carry a credential a run would actually spend? The
    /// [`credential`](Self::credential) column, judged by the one rule there is:
    /// every spelling but [`MISSING`] and [`NOT_REQUIRED`] is a secret
    /// `fetch_cred` found — the two this build knows beside `stored`
    /// (`ambient`, `inline`) and any it has never heard of.
    ///
    /// **One predicate, one home** (bl-dba3). The §8.1 sign-in gate
    /// (`boundary::dispatch::signin`) asks this question of the same column,
    /// and the rendered row used to answer it a second way — a stat of
    /// `<credentials-dir>/<name>.json`, which is blind to `ambient` and `inline`
    /// by construction. Two representations of one fact, and they drifted: a row
    /// answering live off an ambient credential rendered *not signed in*.
    pub fn credentialed(&self) -> bool {
        self.credential != MISSING && self.credential != NOT_REQUIRED
    }

    /// The credential fact in the words this row's credential model makes true
    /// (bl-402f, STORIES S5 point 4 "presence renders"): a login-capable row is
    /// signed in or not, a keyless row needs nothing, and a keyed row's secret
    /// is there or absent — "signed in" is a sentence only a login-capable row
    /// can earn. The boolean is [`credentialed`](Self::credentialed), brazen's
    /// own answer to whether the row could answer at all.
    fn credential_words(&self) -> &'static str {
        match (self.auth.as_str(), self.credentialed()) {
            (OAUTH2, true) => "signed in",
            (OAUTH2, false) => "not signed in",
            (NONE, _) => "no credential needed",
            (_, true) => "credential stored",
            (_, false) => "no credential stored",
        }
    }

    /// This row as every surface renders it (§8.3, §9.5).
    fn view(&self) -> ProviderRowView {
        ProviderRowView {
            name: self.name.clone(),
            fact: format!("auth {} · {}", self.auth, self.credential_words()),
            blocked: self.login_blocked(),
            effort: self.effort,
            priority: self.priority,
        }
    }
}

/// One provider row as the operator reads it: what it is called, its credential
/// fact in words, and either the Login verb (`blocked == None`) or the reason
/// there is none. **One derivation, one painted seat** — the §8.3 Login pane
/// renders this struct and, since bl-20cb, nothing else does: a second surface
/// painting the same row was two renderings of one fact, and the seat that keeps
/// it is the one whose verb the row is about.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderRowView {
    pub name: String,
    pub fact: String,
    pub blocked: Option<String>,
    /// Whether this row takes `/effort` (§9.4's tuning pair, bl-23bd) — the
    /// per-row capability a controls surface shows the selector under, and
    /// hides it under otherwise. **It gates a control, never a write**: the
    /// config field is always lawful, and a level a model declines is the
    /// provider's own refusal in the seat's banner, said rather than gated
    /// (§9.4's caveat discipline).
    pub effort: bool,
    /// Whether this row takes `/priority` — the same, over the priority lane.
    pub priority: bool,
}

/// The effective table as the rendered rows, in listing order. Every fact a
/// surface states about a provider is a column of the row itself (bl-dba3), so
/// this is a projection and takes nothing beside the table.
pub fn row_views(rows: &[ProviderRow]) -> Vec<ProviderRowView> {
    rows.iter().map(ProviderRow::view).collect()
}

/// Parse `bz --list-providers --json`
/// (`{"providers":[{"name":…,"protocol":…,"auth":…},…]}`) into rows, in listing
/// order — which IS brazen's routing order. A non-object or shapeless payload
/// folds to no rows, never an error; a row missing any column folds to an empty
/// string for it (an unnamed row is unroutable, an unknown auth model is not
/// `oauth2`, and an unreadable protocol answers nothing about tools, so all
/// three degrade without a branch).
pub fn provider_rows(listing_json: &str) -> Vec<ProviderRow> {
    let Ok(listing) = serde_json::from_str::<serde_json::Value>(listing_json) else {
        return Vec::new();
    };
    let Some(rows) = listing
        .get("providers")
        .and_then(serde_json::Value::as_array)
    else {
        return Vec::new();
    };
    rows.iter()
        .map(|row| ProviderRow {
            name: column(row, "name"),
            protocol: column(row, "protocol"),
            auth: column(row, "auth"),
            credential: column(row, "credential"),
            effort: flag(row, "effort"),
            priority: flag(row, "priority"),
            tools: row.get("tools").and_then(serde_json::Value::as_bool),
            device: column(row, "device"),
        })
        .collect()
}

/// The `name` column alone, in listing order — the whole answer a caller that
/// only asks "which rows exist" needs (§9.4's pick gate, §9.5's provider
/// control). Kept here so the
/// projection has one home: a caller that mapped the rows itself would be a
/// second place that knows which column names a row.
pub fn row_names(rows: &[ProviderRow]) -> Vec<String> {
    rows.iter().map(|r| r.name.clone()).collect()
}

/// One boolean column of a listing row, `false` when absent or not a boolean.
///
/// **Absent is `false`, and that is the honest reading rather than a shrug.**
/// The column is a *capability declaration*: a listing that does not carry it
/// has declared nothing, and a surface that offered the control anyway would be
/// advertising a knob nothing has said this row can take — the inverse of the
/// rule the string columns keep, where a question that went unanswered is never
/// a refusal. The asymmetry is the direction of the claim: an unanswered
/// question may not *block* a row, and it may not *grant* it a control either.
fn flag(row: &serde_json::Value, key: &str) -> bool {
    row.get(key)
        .and_then(serde_json::Value::as_bool)
        .unwrap_or_default()
}

/// One string column of a listing row, or `""` when absent or not a string.
fn column(row: &serde_json::Value, key: &str) -> String {
    row.get(key)
        .and_then(serde_json::Value::as_str)
        .unwrap_or_default()
        .to_owned()
}

mod capability;

pub use capability::dialect_decline;

#[cfg(test)]
mod tests;