Skip to content

Commit eb75a8a

Browse files
tildesrcPanopticon Agentclaude
authored
Materialize codex credentials β€” auth.json + file-backed cred store (#393)
* Materialize codex credentials β€” auth.json + file-backed cred store The codex adapter only *checked* OPENAI_API_KEY in the env and wired nothing. Codex authenticates from $CODEX_HOME/auth.json (and may otherwise reach for an OS keyring the container lacks), so a bare env var never logged codex in β€” a launch blocker. Add a `write_credentials` seam to the AgentCLI ABC (claude: no-op; codex: render auth.json from CODEX_API_KEY/OPENAI_API_KEY in the exact shape `codex login --with-api-key` writes, 0600, idempotent β€” never clobbering an existing login β€” and pin `cli_auth_credentials_store = "file"`). The launcher calls it after the auth check, before launch β€” it holds no CLI-specific credential shape. Fix `auth_missing_detail`: it now takes the config dir and accepts any of CODEX_API_KEY / OPENAI_API_KEY / CODEX_ACCESS_TOKEN *or* a pre-existing auth.json on the per-task volume (a container already logged in β€” e.g. carried across respawn β€” was wrongly failed before). The detail names every option. Document the codex/OpenAI tiers in docs/auth.md and fix the stale "auth β†’ OPENAI_API_KEY" docstrings. Out of scope: the shared credential-dir mount for rotating ChatGPT subscription tokens (its own subtask). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Reorient auth docs around per-repo agent_cli (claude vs codex) The page opened "Every task runs claude", which is wrong for tasks whose repo sets agent_cli=codex. Restructure into per-CLI sections: a generalized intro, "Claude (the default)" wrapping the existing flow, and a first-class "Codex (tasks whose agent_cli is codex)" section framed as the parallel per-repo env-file setup (API key or ChatGPT workspace token), plus a codex rotating-key gotcha (auth.json is cached on the per-task volume). Shared notes are retitled "both CLIs" with claude-specific items tagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Panopticon Agent <agent@panopticon.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ece94ff commit eb75a8a

9 files changed

Lines changed: 239 additions & 40 deletions

File tree

β€Ždocs/auth.mdβ€Ž

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
# Container authentication β€” giving tasks a `claude` token
1+
# Container authentication β€” giving a task's agent CLI its credentials
22

3-
Every task runs `claude` inside its container. The agent authenticates from a
4-
**`CLAUDE_CODE_OAUTH_TOKEN`** environment variable, which the runner injects from the **repo's
5-
`env_file`** at spawn (ADR 0007 / ADR 0012). You provide that token once per repo; it is long-lived
6-
and non-rotating, so it survives concurrent tasks and respawns (no ~8h re-login cliff).
3+
Every task runs an agent CLI inside its container β€” **`claude` by default, or `codex`** when the
4+
repo's `agent_cli` selects it. Either way the agent authenticates from credentials the runner injects
5+
from the **repo's `env_file`** at spawn (ADR 0007 / ADR 0012); which variable(s) you set depends on
6+
the CLI. This page covers **claude** first (the default), then **codex** β€” tasks whose `agent_cli`
7+
is `codex`.
8+
9+
## Claude (the default)
10+
11+
Claude authenticates from a **`CLAUDE_CODE_OAUTH_TOKEN`** environment variable, which the runner
12+
injects from the repo's `env_file`. You provide that token once per repo; it is long-lived and
13+
non-rotating, so it survives concurrent tasks and respawns (no ~8h re-login cliff).
714

815
Normally you don't set this up by hand: **`panopticon quickstart` registers the repo and drops you
9-
into a `setup-repo` task** that mints the token and writes it into the env-file for you. This page is
10-
the deep-dive and the manual path β€” set it up by hand (mint with the `claude` CLI, drop the token
16+
into a `setup-repo` task** that mints the token and writes it into the env-file for you. This section
17+
is the deep-dive and the manual path β€” set it up by hand (mint with the `claude` CLI, drop the token
1118
into the env-file β€” below), or run the **`setup-repo` workflow** on its own (see *The `setup-repo`
1219
workflow* below). There is no `login` command.
1320

14-
## One-time setup per account
21+
### One-time setup per account
1522

1623
1. **Mint a long-lived token** on a machine where you can complete the browser OAuth (it needs a
1724
Claude subscription or Console login):
@@ -47,7 +54,7 @@ workflow* below). There is no `login` command.
4754

4855
That's it β€” new task containers for that repo now authenticate from the token.
4956

50-
## The `setup-repo` workflow
57+
### The `setup-repo` workflow
5158

5259
`panopticon quickstart` runs this workflow for you. To do it manually, start a **`setup-repo`** task
5360
from the repos modal β€” press `g` on the dashboard, highlight the repo, and press `s`.
@@ -59,19 +66,54 @@ the previous line is **commented out** (kept as a record, not deleted) and any p
5966
it can't capture the token (or the repo has no `env_file`), it falls back to printing the copy-it-in
6067
instructions above.
6168

62-
## Notes
69+
## Codex (tasks whose `agent_cli` is `codex`)
70+
71+
A repo whose `agent_cli` is `codex` runs the `codex` CLI in its task containers instead of `claude`,
72+
and codex authenticates differently: it reads credentials from `$CODEX_HOME/auth.json`, not from an
73+
env var directly, and otherwise reaches for an OS keyring the container doesn't have. The container
74+
adapter bridges this β€” it pins codex to the **file** credential store and, on the container's first
75+
launch, materializes `auth.json` from whatever key you put in the repo's env-file.
76+
77+
So setup is the same shape as claude's: **add one line to the repo's `env_file`** (see *Add it to the
78+
repo's env-file* above for how to create one and point the repo at it). Choose **one**:
79+
80+
- **API key** β€” `OPENAI_API_KEY=sk-…` (or `CODEX_API_KEY=sk-…`; both spellings are accepted). The
81+
standard API-billed key from platform.openai.com. On first launch the adapter writes
82+
`$CODEX_HOME/auth.json` as `{"auth_mode": "apikey", "OPENAI_API_KEY": "…"}` (mode `0600`).
83+
- **ChatGPT workspace token** β€” `CODEX_ACCESS_TOKEN=…`, a ChatGPT Business/Enterprise workspace
84+
access token (minted at chatgpt.com/admin β†’ access tokens), the analog of `claude setup-token`.
85+
Codex reads it straight from the env; no file is written.
86+
87+
That's it β€” new codex task containers for that repo now authenticate from the env-file. There is no
88+
`setup-repo` equivalent for codex yet, so add the line by hand.
89+
90+
Notes specific to codex:
91+
92+
- **Idempotent, never clobbered.** If `auth.json` already exists on the per-task config volume (a
93+
container already logged in, e.g. carried across respawn), the adapter leaves it untouched β€” and a
94+
container that has only a persisted `auth.json` still counts as authenticated.
95+
- **No validation up front.** We only check that a credential is *present*; an invalid or expired key
96+
surfaces at codex's first call, not at launch.
97+
- **Rotating an API key.** Because `auth.json` lives on the per-task config volume and is written
98+
once (never clobbered), changing `OPENAI_API_KEY` in the env-file and respawning **won't** re-auth
99+
an existing task β€” its `auth.json` is already there. New tasks pick up the new key; to rotate a
100+
live one, clear its `auth.json` from the per-task volume before respawn. (`CODEX_ACCESS_TOKEN`,
101+
read from the env, has no such caching β€” respawn picks up a change.)
102+
103+
## Notes (both CLIs)
63104

64105
- **The env-file lives on the host that spawns the container.** Because `env_file` is stored as a
65106
bare name resolved against each runner's own `~/.config/panopticon/secrets/`, the same repo record
66107
works across hosts: with a single host (M1) that's the machine you minted on; with remote runners
67108
(M5), place a same-named env-file under each runner host's secrets dir.
68-
- **`ANTHROPIC_API_KEY` overrides `CLAUDE_CODE_OAUTH_TOKEN`.** If a repo needs to burst past the
69-
subscription rate limit, put an `ANTHROPIC_API_KEY` in the same env-file β€” but don't set both
109+
- **`ANTHROPIC_API_KEY` overrides `CLAUDE_CODE_OAUTH_TOKEN` (claude).** If a repo needs to burst past
110+
the subscription rate limit, put an `ANTHROPIC_API_KEY` in the same env-file β€” but don't set both
70111
unintentionally, since the API key wins.
71-
- **Already-running tasks** keep their old token until they respawn. After editing the env-file,
72-
respawn a live task from the dashboard (`R`) to pick up the new value.
73-
- **Rotating/revoking.** To replace a token, mint a new one and overwrite the env-file line (or
74-
re-run the `setup-repo` workflow, which comments out the old line and appends the new one).
112+
- **Already-running tasks** keep their old credentials until they respawn. After editing the
113+
env-file, respawn a live task from the dashboard (`R`) to pick up the new value (for codex, mind
114+
the `auth.json` caching noted above).
115+
- **Rotating/revoking (claude).** To replace a token, mint a new one and overwrite the env-file line
116+
(or re-run the `setup-repo` workflow, which comments out the old line and appends the new one).
75117
Per-token revocation isn't available upstream (account-level "revoke all" can take time to
76118
propagate), so treat a leak as "mint a replacement + monitor usage in the Console," and keep the
77119
env-file tightly held.

β€Žsrc/panopticon/container/agent.pyβ€Ž

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
The launcher is **CLI-agnostic** (ADR 0014): it resolves an :class:`~panopticon.container.cli.AgentCLI`
1010
adapter from the CLI name the runner passes (``PANOPTICON_AGENT_CLI``, defaulting to ``claude``) and
1111
drives the bootstrap-then-launch sequence against it, holding no ``claude`` literal. Auth is the
12-
adapter's env-var check; the launcher wires no credentials of its own.
12+
adapter's env-var check plus its :meth:`~panopticon.container.cli.AgentCLI.write_credentials` step
13+
(claude reads its token from the env; codex materializes ``auth.json``) β€” the launcher itself knows
14+
no CLI-specific credential shape.
1315
1416
The container's entrypoint (`python -m panopticon.container`) holds the liveness connection;
1517
this runs alongside it in the tmux pane, so `tmux attach` reaches the live agent.
@@ -62,7 +64,7 @@ def main(
6264
config_dir = (home or Path.home()) / cli.config_dirname
6365
task_id = env["PANOPTICON_TASK_ID"]
6466
runner_id = env.get("PANOPTICON_RUNNER_ID")
65-
detail = cli.auth_missing_detail(env)
67+
detail = cli.auth_missing_detail(env, config_dir)
6668
if detail is not None:
6769
if runner_id:
6870
client.report_lifecycle(task_id, runner_id, phase="failed", detail=detail)
@@ -75,6 +77,9 @@ def main(
7577
config_dir, client.workflow_overview(task_id)
7678
) # β†’ the agent's context (the map)
7779
cli.trust_workspace(config_dir, Path.cwd()) # pre-accept the trust dialog (no operator to)
80+
cli.write_credentials(
81+
config_dir, env
82+
) # materialize on-disk creds (codex auth.json; claude no-op)
7883
(launch or cli.launch)(config_dir) # the agent runs until it exits...
7984
on_exit() # ...then stop the container (task β†’ down β†’ respawn)
8085

β€Žsrc/panopticon/container/cli/base.pyβ€Ž

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,21 @@ def trust_workspace(self, config_dir: Path, cwd: Path) -> Path:
6767
"""Pre-accept the CLI's first-run/trust dialogs for ``cwd`` (no operator in the container)."""
6868

6969
@abstractmethod
70-
def auth_missing_detail(self, env: Mapping[str, str]) -> str | None:
71-
"""The failure detail if the CLI's auth env var is absent, else ``None`` (auth is present)."""
70+
def auth_missing_detail(self, env: Mapping[str, str], config_dir: Path) -> str | None:
71+
"""The failure detail if the CLI can't authenticate, else ``None``.
72+
73+
Auth is present when the CLI's env var is set **or** a persisted credential already sits on
74+
the per-task config volume (``config_dir``) β€” so a container carried across respawn isn't
75+
wrongly failed. Presence check only; validity surfaces at the CLI's first call.
76+
"""
77+
78+
@abstractmethod
79+
def write_credentials(self, config_dir: Path, env: Mapping[str, str]) -> Path | None:
80+
"""Materialize any on-disk credentials the CLI needs from the env (idempotent).
81+
82+
Returns the written path, or ``None`` when the CLI reads its credentials straight from the
83+
env (claude) or a credential file is already present. Never clobbers an existing one.
84+
"""
7285

7386
@abstractmethod
7487
def resolve_model(self, tier: str) -> str:

β€Žsrc/panopticon/container/cli/claude.pyβ€Ž

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,23 @@ def trust_workspace(self, config_dir: Path, cwd: Path) -> Path:
113113
projects.setdefault(str(cwd), {})["hasTrustDialogAccepted"] = True
114114
return config
115115

116-
def auth_missing_detail(self, env: Mapping[str, str]) -> str | None:
116+
def auth_missing_detail(self, env: Mapping[str, str], config_dir: Path) -> str | None:
117117
"""The failure detail when neither claude auth env var is set, else ``None``.
118118
119119
Auth is the ``CLAUDE_CODE_OAUTH_TOKEN`` env var the runner injects from the repo's
120-
``env_file`` (an ``ANTHROPIC_API_KEY`` is also sufficient); the launcher wires no credentials.
120+
``env_file`` (an ``ANTHROPIC_API_KEY`` is also sufficient); claude reads it straight from the
121+
env, so there's no persisted credential file to fall back on β€” ``config_dir`` is unused.
121122
"""
122123
if env.get("CLAUDE_CODE_OAUTH_TOKEN") or env.get("ANTHROPIC_API_KEY"):
123124
return None
124125
return (
125126
"No auth token β€” set CLAUDE_CODE_OAUTH_TOKEN in the repo's env_file (see docs/auth.md)"
126127
)
127128

129+
def write_credentials(self, config_dir: Path, env: Mapping[str, str]) -> Path | None:
130+
"""No-op: claude authenticates from the env var itself, with no on-disk credential to write."""
131+
return None
132+
128133
def resolve_model(self, tier: str) -> str:
129134
"""Map the control plane's abstract model tier to claude's concrete model id (ADR 0014 Β§3a).
130135

β€Žsrc/panopticon/container/cli/codex.pyβ€Ž

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
``/workspace/AGENTS.md``), which layers additively on top of the repo's own instructions;
1313
- **trust / unattended posture** β†’ ``config.toml`` (project ``trust_level`` + ``approval_policy`` /
1414
``sandbox_mode``) so a headless container isn't blocked, on first run *and* on resume;
15-
- **auth** β†’ ``OPENAI_API_KEY``;
15+
- **auth** β†’ an API key (``CODEX_API_KEY`` / ``OPENAI_API_KEY``) materialized into
16+
``$CODEX_HOME/auth.json`` (a bare env var does *not* log codex in), or a ChatGPT workspace
17+
access token (``CODEX_ACCESS_TOKEN``) read straight from the env β€” see :meth:`write_credentials`;
1618
- **launch / resume** β†’ ``codex`` first-run vs ``codex resume --last`` (the ``claude --continue``
1719
analogue), probing ``$CODEX_HOME/sessions`` for a prior transcript.
1820
@@ -72,6 +74,12 @@ class CodexAgentCLI(AgentCLI):
7274
WORKFLOW_OVERVIEW_FILE: ClassVar[str] = "AGENTS.md"
7375
#: Session transcripts live here under the config dir; their presence means "resume" (Β§ launch).
7476
SESSIONS_DIRNAME: ClassVar[str] = "sessions"
77+
#: codex's credentials file under the config home β€” what ``codex login --with-api-key`` writes.
78+
AUTH_FILE: ClassVar[str] = "auth.json"
79+
#: Env-var spellings carrying an OpenAI API key we materialize into :attr:`AUTH_FILE`.
80+
API_KEY_VARS: ClassVar[tuple[str, ...]] = ("CODEX_API_KEY", "OPENAI_API_KEY")
81+
#: The ChatGPT workspace access token (the ``claude setup-token`` analog); read from the env, no file.
82+
ACCESS_TOKEN_VAR: ClassVar[str] = "CODEX_ACCESS_TOKEN"
7583

7684
def render_skills(self, client: _Client, task_id: str, home: Path) -> list[Path]:
7785
"""Render the workflow's skills to ``~/.agents/skills/`` (codex's model-discoverable surface)."""
@@ -165,15 +173,59 @@ def trust_workspace(self, config_dir: Path, cwd: Path) -> Path:
165173
projects.setdefault(str(cwd), {})["trust_level"] = "trusted"
166174
return config
167175

168-
def auth_missing_detail(self, env: Mapping[str, str]) -> str | None:
169-
"""The failure detail when codex's auth env var is absent, else ``None``.
176+
def auth_missing_detail(self, env: Mapping[str, str], config_dir: Path) -> str | None:
177+
"""The failure detail when codex has no way to authenticate, else ``None``.
178+
179+
Codex is satisfied by any of the auth vars the runner injects from the repo's ``env_file``
180+
(ADR 0007 / 0012 generalize per CLI) β€” an API key (``CODEX_API_KEY`` / ``OPENAI_API_KEY``,
181+
which :meth:`write_credentials` materializes into ``auth.json``) or a ChatGPT workspace access
182+
token (``CODEX_ACCESS_TOKEN``, read straight from the env) β€” **or** a pre-existing
183+
``auth.json`` on the per-task config volume (a container already logged in, e.g. carried
184+
across respawn β€” which a bare env check would wrongly fail). Presence checks only: we don't
185+
validate the key shape (OpenAI's format isn't ours to pin); an invalid credential surfaces at
186+
codex's first call.
187+
"""
188+
if any(env.get(var) for var in (*self.API_KEY_VARS, self.ACCESS_TOKEN_VAR)):
189+
return None
190+
if (config_dir / self.AUTH_FILE).exists():
191+
return None
192+
return (
193+
"No codex auth β€” set OPENAI_API_KEY (or CODEX_API_KEY / CODEX_ACCESS_TOKEN) in the "
194+
"repo's env_file (see docs/auth.md)"
195+
)
196+
197+
def write_credentials(self, config_dir: Path, env: Mapping[str, str]) -> Path | None:
198+
"""Materialize codex's ``auth.json`` from an API key in the env, and pin the file cred store.
199+
200+
A bare ``OPENAI_API_KEY`` in the container env does **not** log codex in β€” codex
201+
authenticates from ``$CODEX_HOME/auth.json`` and may otherwise reach for an OS keyring the
202+
container lacks. So we:
170203
171-
Auth is ``OPENAI_API_KEY``, injected by the runner from the repo's ``env_file`` (ADR 0007 /
172-
0012 generalize per CLI); the launcher wires no credentials.
204+
- set ``cli_auth_credentials_store = "file"`` (top-level ``config.toml``) so codex reads
205+
credentials from the file, never a keyring β€” done unconditionally, so it also governs a
206+
pre-existing ``auth.json`` carried across respawn;
207+
- when ``auth.json`` is absent, render it from ``CODEX_API_KEY`` or ``OPENAI_API_KEY`` in the
208+
exact shape ``codex login --with-api-key`` writes β€” ``{"auth_mode": "apikey",
209+
"OPENAI_API_KEY": <key>}`` β€” at mode ``0600``.
210+
211+
**Idempotent: an existing ``auth.json`` is never clobbered**, so a container already logged in
212+
keeps its credentials. Returns the ``auth.json`` path when written, else ``None`` (no API key,
213+
or one already present). A workspace access token (``CODEX_ACCESS_TOKEN``) needs no file β€”
214+
codex reads it from the env β€” so it doesn't trigger a write here (the auth gate accepts it).
173215
"""
174-
if env.get("OPENAI_API_KEY"):
216+
config = config_dir / self.CONFIG_FILE
217+
with update_toml_config(config) as data:
218+
data["cli_auth_credentials_store"] = "file"
219+
auth = config_dir / self.AUTH_FILE
220+
if auth.exists():
175221
return None
176-
return "No auth token β€” set OPENAI_API_KEY in the repo's env_file (see docs/auth.md)"
222+
key = next((env[var] for var in self.API_KEY_VARS if env.get(var)), None)
223+
if not key:
224+
return None
225+
config_dir.mkdir(parents=True, exist_ok=True)
226+
auth.write_text(json.dumps({"auth_mode": "apikey", "OPENAI_API_KEY": key}))
227+
auth.chmod(0o600)
228+
return auth
177229

178230
def resolve_model(self, tier: str) -> str:
179231
"""Map the control plane's abstract model tier to codex's concrete model id (ADR 0014 Β§3a).

β€Žtests/container/test_agent.pyβ€Ž

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class _FakeCLI(ClaudeAgentCLI):
9090
name = "fake"
9191
config_dirname = ".fake"
9292

93-
def auth_missing_detail(self, env: object) -> str | None:
93+
def auth_missing_detail(self, env: object, config_dir: object) -> str | None:
9494
calls.append("auth")
9595
return None
9696

@@ -118,6 +118,10 @@ def trust_workspace(self, config_dir: Path, cwd: Path) -> Path:
118118
calls.append("trust")
119119
return config_dir
120120

121+
def write_credentials(self, config_dir: Path, env: object) -> Path | None:
122+
calls.append("credentials")
123+
return None
124+
121125
def launch(self, config_dir: Path) -> None:
122126
calls.append(f"launch:{config_dir}")
123127

@@ -136,6 +140,7 @@ def launch(self, config_dir: Path) -> None:
136140
f"mcp:{tmp_path / '.fake'}",
137141
"overview",
138142
"trust",
143+
"credentials",
139144
f"launch:{tmp_path / '.fake'}",
140145
"on_exit",
141146
]

β€Žtests/container/test_claude.pyβ€Ž

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,17 @@ def test_trust_workspace_merges_and_is_idempotent(tmp_path: Path) -> None:
227227
# -- auth env check -----------------------------------------------------------------------------
228228

229229

230-
def test_auth_missing_detail_flags_the_absent_token() -> None:
230+
def test_auth_missing_detail_flags_the_absent_token(tmp_path: Path) -> None:
231231
cli = ClaudeAgentCLI()
232-
assert cli.auth_missing_detail({}) is not None
233-
assert "CLAUDE_CODE_OAUTH_TOKEN" in (cli.auth_missing_detail({}) or "")
234-
assert cli.auth_missing_detail({"CLAUDE_CODE_OAUTH_TOKEN": "sk"}) is None
235-
assert cli.auth_missing_detail({"ANTHROPIC_API_KEY": "sk"}) is None # either is sufficient
232+
assert cli.auth_missing_detail({}, tmp_path) is not None
233+
assert "CLAUDE_CODE_OAUTH_TOKEN" in (cli.auth_missing_detail({}, tmp_path) or "")
234+
assert cli.auth_missing_detail({"CLAUDE_CODE_OAUTH_TOKEN": "sk"}, tmp_path) is None
235+
assert cli.auth_missing_detail({"ANTHROPIC_API_KEY": "sk"}, tmp_path) is None # either suffices
236+
237+
238+
def test_write_credentials_is_a_no_op_for_claude(tmp_path: Path) -> None:
239+
# claude reads its token from the env; there's no on-disk credential to materialize.
240+
assert ClaudeAgentCLI().write_credentials(tmp_path, {"CLAUDE_CODE_OAUTH_TOKEN": "sk"}) is None
236241

237242

238243
# -- hook payload seam (background-task gating) --------------------------------------------------

β€Žtests/container/test_cli_base.pyβ€Ž

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ def write_workflow_overview(self, config_dir: Path, overview: str) -> Path | Non
5757
def trust_workspace(self, config_dir: Path, cwd: Path) -> Path:
5858
return config_dir
5959

60-
def auth_missing_detail(self, env: object) -> str | None:
60+
def auth_missing_detail(self, env: object, config_dir: object) -> str | None:
61+
return None
62+
63+
def write_credentials(self, config_dir: Path, env: object) -> Path | None:
6164
return None
6265

6366
def resolve_model(self, tier: str) -> str:

0 commit comments

Comments
Β (0)