Skip to content

Vendor files

vendor_files

Pydantic models describing the credential files we read from third-party CLIs.

These files are owned by Anthropic, OpenAI, GitHub, GitLab, and a handful of OpenAI-compatible providers — not by us. Every model in this module therefore uses extra="ignore" (Pydantic's default), and only the fields we actually consume have any guarantees. Vendor adds a new key, prunes a side-field, renames an internal-only block? Best-effort: we keep working as long as the fields we read still hold their shape.

A single failure mode is loud: if a vendor renames or retypes a field we depend on (e.g. claudeAiOauth.accessToken or tokens.access_token), the model raises ValidationError — pointing at the exact field, in the exact file. Callers translate that into a clear "vendor file format may have changed" surface.

For fields where we tolerate absence (most of them), the field is declared optional with a default; the extractor checks truthiness rather than is not None.

The file-loading helpers in this module (load_vendor_json, load_vendor_yaml) distinguish between "file is absent / unreadable / not a dict at the top level" (silent fallback) and "file present but structure broke our contract" (loud). See those docstrings for the exact rules.

JsTimestamp = Annotated[float | None, BeforeValidator(_normalize_js_timestamp)] module-attribute

POSIX-seconds timestamp coerced from a JS Date.now() ms value.

__all__ = ['JsTimestamp', 'RawApiKeyJsonFile', 'RawClaudeCredentialsFile', 'RawClaudeOauthBlock', 'RawCodexAuthFile', 'RawCodexTokensBlock', 'RawGhHostBlock', 'RawGhHostsFile', 'RawGlabConfigFile', 'RawGlabHostBlock', 'load_vendor_json', 'load_vendor_yaml', 'warn_drift'] module-attribute

RawClaudeOauthBlock

Bases: _VendorFile

.credentials.jsonclaudeAiOauth — Claude OAuth state block.

Typed fields are the ones we actually inspect: accessToken / refreshToken go into HTTP headers, expiresAt drives the refresh timer. Everything else is pass-through metadata stored in the output credential dict — declared as :data:typing.Any to avoid coupling to a vendor-side shape we never look at.

accessToken = '' class-attribute instance-attribute

refreshToken = '' class-attribute instance-attribute

expiresAt = None class-attribute instance-attribute

scopes = '' class-attribute instance-attribute

subscriptionType = None class-attribute instance-attribute

rateLimitTier = None class-attribute instance-attribute

RawClaudeCredentialsFile

Bases: _VendorFile

Top-level shape of Claude Code's .credentials.json.

The OAuth block is optional — the file may exist without it (e.g. when the user authenticated via API key only).

claudeAiOauth = None class-attribute instance-attribute

RawCodexTokensBlock

Bases: _VendorFile

auth.jsontokens — Codex OAuth token block.

access_token and refresh_token go into HTTP headers; id_token is parsed as a JWT in the synthetic-auth-file writer. account_id is pass-through metadata, declared as :data:typing.Any.

access_token = '' class-attribute instance-attribute

refresh_token = '' class-attribute instance-attribute

id_token = None class-attribute instance-attribute

account_id = None class-attribute instance-attribute

RawCodexAuthFile

Bases: _VendorFile

Top-level shape of Codex's auth.json.

Both tokens (OAuth) and OPENAI_API_KEY (legacy) are optional; the extractor accepts whichever is present.

tokens = None class-attribute instance-attribute

OPENAI_API_KEY = None class-attribute instance-attribute

RawApiKeyJsonFile

Bases: _VendorFile

{"api_key": "..."}-shaped JSON config.

Used by Claude's config.json (API-key fallback path) and by the OpenAI-compatible providers (blablador, kisski).

api_key = '' class-attribute instance-attribute

RawGhHostBlock

Bases: _VendorFile

hosts.yml<host> — one entry in gh's per-host config.

oauth_token = '' class-attribute instance-attribute

RawGhHostsFile

Bases: RootModel[dict[str, RawGhHostBlock]]

Top-level shape of gh's hosts.yml.

The YAML is a bare dict keyed by host name (github.com, ghe.example.com, …) — no wrapper section. RootModel lets us validate it without inventing a synthetic outer key.

RawGlabHostBlock

Bases: _VendorFile

config.ymlhosts.<host> — one entry in glab's per-host config.

token = '' class-attribute instance-attribute

RawGlabConfigFile

Bases: _VendorFile

Top-level shape of glab's config.yml — has a hosts: map.

hosts = Field(default_factory=dict) class-attribute instance-attribute

load_vendor_json(model, path)

Read a JSON vendor file from path and validate against model.

Returns None only for "the file isn't there" cases — missing, unreadable, or unparseable as JSON. When the JSON parses but the structure doesn't match model (wrong root type, missing nested field, wrong field type), raises ValidationError — a ValueError subclass — so the caller surfaces "your credential file is in a shape we don't recognize" rather than silently falling through to a generic "not found" path. Callers that want a fall-through anyway (e.g. Claude tries OAuth then API key) catch ValidationError explicitly.

Source code in src/terok_executor/credentials/vendor_files.py
def load_vendor_json[T: BaseModel](model: type[T], path: Path) -> T | None:
    """Read a JSON vendor file from *path* and validate against *model*.

    Returns ``None`` only for "the file isn't there" cases — missing,
    unreadable, or unparseable as JSON.  When the JSON parses but the
    structure doesn't match *model* (wrong root type, missing nested
    field, wrong field type), raises [`ValidationError`][pydantic_core.ValidationError]
    — a ``ValueError`` subclass — so the caller surfaces "your credential
    file is in a shape we don't recognize" rather than silently falling
    through to a generic "not found" path.  Callers that want a
    fall-through anyway (e.g. Claude tries OAuth then API key) catch
    [`ValidationError`][pydantic_core.ValidationError] explicitly.
    """
    try:
        text = path.read_text(encoding="utf-8")
        data = json.loads(text)
    except (OSError, UnicodeDecodeError, json.JSONDecodeError):
        return None
    return model.model_validate(data)

load_vendor_yaml(model, path)

Read a YAML vendor file from path and validate against model.

Same fallback / loud-fail rules as load_vendor_json. Uses ruamel.yaml's safe loader; RootModel subclasses (like RawGhHostsFile) accept the parsed dict as their root value.

Source code in src/terok_executor/credentials/vendor_files.py
def load_vendor_yaml[T: BaseModel](model: type[T], path: Path) -> T | None:
    """Read a YAML vendor file from *path* and validate against *model*.

    Same fallback / loud-fail rules as [`load_vendor_json`][terok_executor.credentials.vendor_files.load_vendor_json].
    Uses ruamel.yaml's safe loader; ``RootModel`` subclasses (like
    [`RawGhHostsFile`][terok_executor.credentials.vendor_files.RawGhHostsFile])
    accept the parsed dict as their root value.
    """
    from ruamel.yaml import YAML
    from ruamel.yaml.error import YAMLError

    yaml = YAML(typ="safe")
    try:
        data = yaml.load(path)
    except (OSError, UnicodeDecodeError, YAMLError):
        return None
    return model.model_validate(data)

warn_drift(path, exc)

Print a stderr breadcrumb when a vendor file fails validation.

Extractors with their own fallback path (e.g. Claude tries OAuth then API key) catch ValidationError silently. Without this breadcrumb, a vendor renaming a field we depend on would surface only as a generic "no creds found" with no diagnostic trail.

Pydantic's default str(exc) includes the offending input_value — which can be a credential — so the breadcrumb deliberately renders only the field path(s) from exc.errors(), never the input value.

Source code in src/terok_executor/credentials/vendor_files.py
def warn_drift(path: Path, exc: ValidationError) -> None:
    """Print a stderr breadcrumb when a vendor file fails validation.

    Extractors with their own fallback path (e.g. Claude tries OAuth then
    API key) catch [`ValidationError`][pydantic_core.ValidationError] silently.
    Without this breadcrumb, a vendor renaming a field we depend on would
    surface only as a generic "no creds found" with no diagnostic trail.

    Pydantic's default ``str(exc)`` includes the offending ``input_value``
    — which can be a credential — so the breadcrumb deliberately renders
    only the field path(s) from ``exc.errors()``, never the input value.
    """
    fields = ", ".join(".".join(str(p) for p in err["loc"]) for err in exc.errors())
    detail = f" at: {fields}" if fields else ""
    print(
        f"Warning [credentials]: {path} has unexpected shape — "
        f"vendor format may have changed.{detail}",
        file=sys.stderr,
    )