mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(providers): Codex CLI provider — OpenAI via ModelProvider.OPENAI Mirrors the grok blueprint end to end: CodexCliProvider (RO ~/.codex mount, ANTHROPIC_* blanked), an orchestrator-side codex_auth.py refresher (JWT-exp staleness, atomic rewrite, lock-serialized single-use rotation, --check backstop; the CLI's own in-process refresh write no-ops on the RO mount by design — margins keep the orchestrator ahead of the CLI's 5-minute window), config.toml rendering with required=true gateway MCP servers, execpolicy deny rules (forbidden-only), per-role --sandbox (developer=workspace-write, review/doc roles read-only), codex exec --json with pinned ROBOCO_CODEX_CLI_MODEL (gpt-5.3-codex), usage summed from typed turn.completed events priced via the real 4-bucket split, dedicated image + entrypoint, registry/park/finalize/ compose/release wiring. V1 excludes interactive intake/secretary. Per adversarial review: migration 083 seeds the openai provider row enabled=True (without it every routing path 404'd — the whole feature was operationally dead code; grok needed the same seed in 039), the panel picker gained the OpenAI catalog group it silently lacked, and exit classification is structural — only stderr and error.message fields from error events are sniffed (word-boundaried patterns, exact auth phrases, bare 'login' dropped), so the model echoing on-topic words can never false-park the provider fleet-wide, proven by a benign-transcript test. Known open risk flagged, not claimed: whether codex's workspace-write OS sandbox excludes /app is unverified, and no hook mechanism exists to port the bash-guard defense-in-depth. * fix(providers): containment barrier on usage.json reads (code scanning) CodeQL flagged the codex usage read as path injection — correctly: os.path.basename does not neutralize '..', and the upstream segment validator isn't in CodeQL's taint model. The grok/codex reads collapse into one _read_usage_json_contained helper that resolves the built path and refuses anything outside the resolved usage root — a hostile id can never escape regardless of upstream drift. Traversal + containment regression tests added; a stray noqa in the test file replaced with a named constant per repo rule. * fix(providers): use realpath+startswith containment CodeQL recognizes The is_relative_to() guard was a real barrier but not in CodeQL's py/path-injection sanitizer model, so the alert persisted. Switch to the canonical os.path.realpath + startswith(root + os.sep) form, which CodeQL recognizes as a path-traversal barrier; behavior is identical (refuse any candidate resolving outside the usage root). * fix(providers): regexp-allowlist the usage-id segment (CodeQL barrier) Neither is_relative_to nor realpath+startswith was recognized by CodeQL's py/path-injection sanitizer model across the str->Path->open flow. Sanitize the tainted component at the source instead: the id must fullmatch a strict slug token ([A-Za-z0-9][A-Za-z0-9._-]*, no separators, no '..'), which CodeQL recognizes as a path-injection barrier; the realpath+startswith containment stays as defense-in-depth. * fix(providers): standalone regexp guard so CodeQL recognizes the barrier The sanitizer was one disjunct of a compound 'or' condition, which CodeQL's guard analysis does not trace as a barrier. Split the regexp fullmatch into its own single-condition guard (the redundant '..' check is dropped — the required alphanumeric first char already excludes it). --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
85 lines
3.4 KiB
Python
85 lines
3.4 KiB
Python
"""Idempotently seed the Codex (OpenAI) provider row.
|
|
|
|
The ``modelprovider`` enum has carried ``'openai'`` since migration
|
|
`004_provider_routing`, but no row was ever seeded for it (unlike GROK's
|
|
`039_seed_grok_provider`) — so any assignment of a catalog model whose
|
|
`provider_type` is OPENAI (`gpt-5.3-codex`) raised `NotFoundError` out of
|
|
`ModelRoutingService._get_seeded_provider`, making the whole Codex provider
|
|
unreachable via the panel the moment an operator tried to route an agent to
|
|
it. This migration is that missing seed.
|
|
|
|
Unlike GROK's row (seeded `enabled=false`, flipped to `true` only by the
|
|
dedicated `apply_mode="grok"` write path), this row seeds `enabled=true`
|
|
directly: there is no `apply_mode="codex"` button — the only way to route to
|
|
Codex is "mix" mode's per-agent picker, which has no equivalent enable step.
|
|
Seeding disabled would leave `resolve_for_agent` silently falling back to the
|
|
legacy Anthropic path forever (`resolved.provider.enabled` gates the route),
|
|
reproducing the exact "silently unreachable" failure this migration exists to
|
|
fix. Codex authenticates via a mounted ChatGPT-subscription `~/.codex`
|
|
directory (see `roboco.llm.providers.codex.CodexCliProvider`), not a stored
|
|
API key, so there is no secret to withhold behind a disabled row anyway —
|
|
`base_url` is seeded for display parity with GROK's row but is blanked before
|
|
the container mount just the same (never used for auth).
|
|
|
|
Revision ID: 083_seed_openai_provider
|
|
Revises: 082_routing_presets
|
|
Create Date: 2026-07-23
|
|
|
|
Note: chains onto ``082_routing_presets``, a sibling branch's revision that
|
|
does not exist in this worktree (this branch was cut before it landed) — the
|
|
same expected-failure posture ``081_doctrine_version`` reported for
|
|
``080_task_project_budgets``. The local migration-graph AND enum-parity tests
|
|
are expected to fail here until this branch integrates alongside 082:
|
|
`test_migration_graph_integrity.py` (dangling down_revision, two heads, an
|
|
unreachable-root walk) and `test_enum_migration_parity.py` (which shells out
|
|
to `alembic upgrade head --sql` and hits the same missing revision id as a
|
|
subprocess `KeyError`, not just a static graph-file check). Re-verify the
|
|
chain resolves to one head at merge time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = "083_seed_openai_provider"
|
|
down_revision = "082_routing_presets"
|
|
branch_labels: dict[str, str] | None = None
|
|
depends_on: dict[str, str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute(
|
|
sa.text(
|
|
"""
|
|
INSERT INTO provider_configs
|
|
(id, name, type, base_url, auth_token_encrypted, enabled, created_at)
|
|
VALUES
|
|
(
|
|
gen_random_uuid(),
|
|
'Codex (OpenAI)',
|
|
'openai',
|
|
'https://api.openai.com/v1',
|
|
NULL,
|
|
true,
|
|
now()
|
|
)
|
|
ON CONFLICT (name) DO NOTHING
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Drop model_assignments pointing at the Codex row first to avoid a FK
|
|
# RESTRICT violation on provider_configs.id.
|
|
op.execute(
|
|
sa.text(
|
|
"DELETE FROM model_assignments "
|
|
"WHERE provider_config_id IN ("
|
|
" SELECT id FROM provider_configs WHERE name = 'Codex (OpenAI)'"
|
|
")"
|
|
)
|
|
)
|
|
op.execute(sa.text("DELETE FROM provider_configs WHERE name = 'Codex (OpenAI)'"))
|