feat(auth): added auth-gateway forward-proxy and broker usage/migrate endpoints

- Added `omp auth-gateway serve/token/status` — a forward-proxy injecting broker credentials for OpenAI Chat, Anthropic Messages, and OpenAI Responses wire formats.
- Added `GET /v1/usage` to auth-broker and auth-gateway; usage cache switched to 5-min per-credential TTL with jitter and last-good fallback on failure.
- Added `AuthStorage.setConfigApiKey/removeConfigApiKey/clearConfigApiKeys` so `models.yml` `apiKey` beats OAuth tokens without overriding `--api-key`.
- Added `omp auth-broker migrate --from-local` for idempotent upload of local SQLite/env credentials to the broker.
This commit is contained in:
can1357
2026-05-16 23:24:33 +02:00
parent d1877baf66
commit df1c1a6ba8
54 changed files with 6802 additions and 227 deletions
+2 -2
View File
@@ -93,8 +93,8 @@ GITHUB_TOKEN=
# =============================================================================
# Either a single model id or a comma-separated pool — roboomp picks one
# uniformly at random per task. Use the `<provider>/<model>` form that matches
# your ~/.omp/agent/models.yml (which is mounted into the container).
ROBOMP_MODEL=p-anthropic/claude-sonnet-4-6
# your ~/.omp/agent/models.container.yml (which is mounted into the container as models.yml).
ROBOMP_MODEL=anthropic/claude-sonnet-4-6
# off|low|medium|high
ROBOMP_THINKING=high
# Optional provider override (passed to `omp --provider`).
+1 -1
View File
@@ -96,7 +96,7 @@ Lint + format: TypeScript via Biome (config in `biome.json`), Python via Ruff (c
- `src/robomp/dashboard.py` — single-page HTML dashboard served from `/`.
- `pyproject.toml` — packaging + pytest config (`asyncio_mode = "auto"`, `testpaths = ["tests"]`).
- `Dockerfile` — slim runtime; consumes `oh-my-pi/artifacts:dev` (built from `/work/pi/Dockerfile`) for `pi_natives.linux-*.node` + `omp_rpc-*.whl`. Tini entrypoint, exposes `8080`, `VOLUME /data`.
- `docker-compose.yml` — `build.args.PI_ARTIFACTS_IMAGE`, mounts `$PI_ROOT:/work/pi:ro`, `./data:/data`, `~/.omp/agent/models.yml:ro`, `extra_hosts: llm-gateway.internal:host-gateway`.
- `docker-compose.yml` — `build.args.PI_ARTIFACTS_IMAGE`, mounts `$PI_ROOT:/work/pi:ro`, `./data:/data`, `~/.omp/agent/models.container.yml:ro` (mapped to `models.yml` inside the container — kept separate from the host's `~/.omp/agent/models.yml` so the host omp doesn't pick up gateway routing intended only for the container), `extra_hosts: llm-gateway.internal:host-gateway`.
- `entrypoint.sh` — validates `PI_ROOT`, creates `/data/{workspaces,logs}` + build caches.
- `.env.example` — authoritative list of required runtime env vars.
- `README.md` — full architecture + operational reference. Authoritative for end-to-end flow, host-tool spec, security posture, and configuration reference.
+12 -12
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.7
# syntax=docker/dockerfile:1.7-labs
###############################################################################
# roboomp — orchestrator image
#
@@ -30,14 +30,14 @@ FROM ${PI_ARTIFACTS_IMAGE} AS pi-artifacts
############################
FROM oven/bun:1.3.14-slim AS web-builder
WORKDIR /work
# The repo is a Bun workspace (`workspaces: ["web"]` at the root). Install
# from the root lockfile so the web subpackage resolves against the same
# pinned dependency graph used locally.
# Build context is the pi monorepo root, so the web-builder stage installs
# from pi's bun.lock — that's how `web/package.json` resolves its `catalog:`
# references against the workspace-wide catalog declared at pi root.
COPY package.json bun.lock ./
COPY web/package.json ./web/package.json
RUN bun install --frozen-lockfile
COPY web/ ./web/
RUN bun --cwd=web run build
COPY python/robomp/web/package.json ./python/robomp/web/package.json
RUN bun install --filter robomp-web
COPY --exclude=node_modules --exclude=dist python/robomp/web/ ./python/robomp/web/
RUN bun --cwd=python/robomp/web run build
############################
# 3) runtime — slim image with everything roboomp needs at boot.
@@ -107,9 +107,9 @@ RUN printf '%s\n' \
# roboomp itself. Drop the Vite-built dashboard into the package tree before
# `pip install` so it lands in the installed wheel (`static/**/*` is declared
# as package-data in pyproject.toml).
COPY pyproject.toml ./
COPY src/ ./src/
COPY --from=web-builder /work/web/dist/ ./src/robomp/static/
COPY python/robomp/pyproject.toml ./
COPY python/robomp/src/ ./src/
COPY --from=web-builder /work/python/robomp/web/dist/ ./src/robomp/static/
RUN pip install --upgrade pip \
&& pip install \
"fastapi>=0.112" "uvicorn[standard]>=0.30" "httpx>=0.27" \
@@ -121,7 +121,7 @@ RUN mkdir -p /srv/agent-home/.agent /srv/agent-home/.omp/agent \
&& mkdir -p /srv/agent-home-stage/.agent /srv/agent-home-stage/.omp/agent \
&& printf '[install]\nbackend = "copyfile"\n' > /srv/agent-home/.bunfig.toml
COPY entrypoint.sh /usr/local/bin/robomp-entrypoint
COPY python/robomp/entrypoint.sh /usr/local/bin/robomp-entrypoint
RUN chmod +x /usr/local/bin/robomp-entrypoint
VOLUME ["/data"]
+2 -2
View File
@@ -47,7 +47,7 @@ into the `tool_calls` table with credential-redacted args and results.
## Setup
Requires Docker Compose v2 and a LiteLLM-style proxy on the host that your
`~/.omp/agent/models.yml` points at. roboomp lives inside the oh-my-pi
`~/.omp/agent/models.container.yml` points at (mounted into the container as `models.yml`; kept under a separate filename on the host so the host omp doesn't route through the gateway). roboomp lives inside the oh-my-pi
monorepo at `python/robomp/`; both the docker build context and the
`/work/pi` bind mount default to the parent monorepo (`../..`). Override
`PI_ROOT` only if you want a different oh-my-pi checkout backing the build
@@ -187,7 +187,7 @@ The integration test spawns a real `omp --mode rpc` against an
| `refusing to push: working tree is dirty` | Uncommitted agent edits. Or just call `gh_open_pr`, which auto-commits `bun run fix` output. |
| `bun check failed before PR creation` | Fix the reported failure and retry `gh_open_pr`. |
| `Failed to load pi_natives` | Wrong arch / missing native. `bun run pi-artifacts` then `bun run build`. |
| `No API key found for <provider>` | `~/.omp/agent/models.yml` mount missing or provider id mismatch with `ROBOMP_MODEL`. |
| `No API key found for <provider>` | `~/.omp/agent/models.container.yml` mount missing or provider id mismatch with `ROBOMP_MODEL`. |
## Layout
+8 -5
View File
@@ -11,11 +11,14 @@ services:
# ───────────────────────────────────────────────────────────────────────────
robomp:
build:
context: .
dockerfile: Dockerfile
# pi root: gives the web-builder stage access to the workspace
# bun.lock + catalog (web/package.json refs `catalog:` versions).
# python/robomp/data is excluded via pi's .dockerignore.
context: ../..
dockerfile: python/robomp/Dockerfile
args:
# Tag of the pre-built artifacts image produced by `bun run pi-artifacts`
# (sources: /work/pi/Dockerfile). Override per-environment as needed.
# (sources: pi root /Dockerfile). Override per-environment as needed.
PI_ARTIFACTS_IMAGE: oh-my-pi/artifacts:dev
image: robomp:dev
container_name: robomp
@@ -41,7 +44,7 @@ services:
ROBOMP_REVIEWER_BOTS: ${ROBOMP_REVIEWER_BOTS:-}
# --- model selection ---
ROBOMP_MODEL: ${ROBOMP_MODEL:-p-anthropic/claude-sonnet-4-6}
ROBOMP_MODEL: ${ROBOMP_MODEL:-anthropic/claude-sonnet-4-6}
ROBOMP_PROVIDER: ${ROBOMP_PROVIDER:-}
ROBOMP_THINKING: ${ROBOMP_THINKING:-high}
@@ -86,7 +89,7 @@ services:
# root-owned, world-readable files under /srv/agent-home; the agent
# subprocess runs with HOME=/srv/agent-home, so ~/.omp and ~/.agent
# resolve there without exposing mutable host mounts.
- ${HOME}/.omp/agent/models.yml:/srv/agent-home-stage/.omp/agent/models.yml:ro
- ${HOME}/.omp/agent/models.container.yml:/srv/agent-home-stage/.omp/agent/models.yml:ro
- ${HOME}/.agent/AGENT.md:/srv/agent-home-stage/.agent/AGENTS.md:ro
- ${HOME}/.agent/rules:/srv/agent-home-stage/.agent/rules:ro
ports:
+1 -1
View File
@@ -57,7 +57,7 @@ class Settings(BaseSettings):
gh_proxy_git_timeout_seconds: float = Field(60.0, alias="ROBOMP_GH_PROXY_GIT_TIMEOUT_SECONDS")
# Model selection
model: str = Field("p-anthropic/claude-sonnet-4-6", alias="ROBOMP_MODEL")
model: str = Field("anthropic/claude-sonnet-4-6", alias="ROBOMP_MODEL")
provider: str | None = Field(None, alias="ROBOMP_PROVIDER")
thinking_level: ThinkingLevel = Field("high", alias="ROBOMP_THINKING")
+4 -4
View File
@@ -102,14 +102,14 @@ def test_model_pool_single(env: dict[str, str]) -> None:
def test_model_pool_csv_parses(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> None:
monkeypatch.setenv(
"ROBOMP_MODEL",
" p-codex/gpt-5.4 , p-anthropic/claude-sonnet-4-6 ,, p-anthropic/claude-opus-4-7 ",
" codex/gpt-5.4 , anthropic/claude-sonnet-4-6 ,, anthropic/claude-opus-4-7 ",
)
reset_settings_cache()
cfg = Settings() # type: ignore[call-arg]
assert cfg.model_pool == (
"p-codex/gpt-5.4",
"p-anthropic/claude-sonnet-4-6",
"p-anthropic/claude-opus-4-7",
"codex/gpt-5.4",
"anthropic/claude-sonnet-4-6",
"anthropic/claude-opus-4-7",
)
+7 -7
View File
@@ -101,21 +101,21 @@ def test_pragma_value_last_wins() -> None:
def test_resolve_model_alias_precedence() -> None:
pool = ("p-anthropic/claude-sonnet-4-6", "p-openai/gpt-5.5", "p-openai/gpt-5.5-mini")
pool = ("anthropic/claude-sonnet-4-6", "openai/gpt-5.5", "openai/gpt-5.5-mini")
# Short-name-after-slash beats substring.
assert resolve_model_alias("gpt-5.5", pool) == "p-openai/gpt-5.5"
assert resolve_model_alias("gpt-5.5", pool) == "openai/gpt-5.5"
# Substring is fallback.
assert resolve_model_alias("gpt", pool) == "p-openai/gpt-5.5"
assert resolve_model_alias("claude", pool) == "p-anthropic/claude-sonnet-4-6"
assert resolve_model_alias("gpt", pool) == "openai/gpt-5.5"
assert resolve_model_alias("claude", pool) == "anthropic/claude-sonnet-4-6"
def test_resolve_model_alias_full_id() -> None:
pool = ("p-openai/gpt-5.5", "p-anthropic/claude-sonnet-4-6")
assert resolve_model_alias("p-openai/gpt-5.5", pool) == "p-openai/gpt-5.5"
pool = ("openai/gpt-5.5", "anthropic/claude-sonnet-4-6")
assert resolve_model_alias("openai/gpt-5.5", pool) == "openai/gpt-5.5"
def test_resolve_model_alias_no_match() -> None:
pool = ("p-anthropic/claude-sonnet-4-6",)
pool = ("anthropic/claude-sonnet-4-6",)
assert resolve_model_alias("gpt", pool) is None
assert resolve_model_alias("", pool) is None
+5 -5
View File
@@ -12,7 +12,7 @@ from robomp.worker import DirectiveInfo, _resolve_pragma_overrides
def settings_with_pool(monkeypatch: pytest.MonkeyPatch, env: dict[str, str]) -> Settings:
monkeypatch.setenv(
"ROBOMP_MODEL",
"p-anthropic/claude-sonnet-4-6,p-openai/gpt-5.5,p-openai/gpt-5.5-mini",
"anthropic/claude-sonnet-4-6,openai/gpt-5.5,openai/gpt-5.5-mini",
)
reset_settings_cache()
return Settings() # type: ignore[call-arg]
@@ -30,14 +30,14 @@ def test_directive_without_pragmas_means_no_override(settings_with_pool: Setting
def test_model_pragma_resolves_to_pool_entry(settings_with_pool: Settings) -> None:
directive = DirectiveInfo(body="run", author="can1357", pragmas=(("model", "gpt"),))
model_override, thinking_override = _resolve_pragma_overrides(directive, settings_with_pool)
assert model_override == "p-openai/gpt-5.5"
assert model_override == "openai/gpt-5.5"
assert thinking_override is None
def test_model_alias_exact_short_name(settings_with_pool: Settings) -> None:
directive = DirectiveInfo(body="run", author="can1357", pragmas=(("model", "gpt-5.5-mini"),))
model_override, _ = _resolve_pragma_overrides(directive, settings_with_pool)
assert model_override == "p-openai/gpt-5.5-mini"
assert model_override == "openai/gpt-5.5-mini"
def test_unmatched_model_alias_falls_back_to_random_pick(settings_with_pool: Settings) -> None:
@@ -66,7 +66,7 @@ def test_both_pragmas_resolved_together(settings_with_pool: Settings) -> None:
pragmas=(("model", "claude"), ("thinking", "medium")),
)
model_override, thinking_override = _resolve_pragma_overrides(directive, settings_with_pool)
assert model_override == "p-anthropic/claude-sonnet-4-6"
assert model_override == "anthropic/claude-sonnet-4-6"
assert thinking_override == "medium"
@@ -77,4 +77,4 @@ def test_last_value_wins_for_duplicate_keys(settings_with_pool: Settings) -> Non
pragmas=(("model", "claude"), ("model", "gpt")),
)
model_override, _ = _resolve_pragma_overrides(directive, settings_with_pool)
assert model_override == "p-openai/gpt-5.5"
assert model_override == "openai/gpt-5.5"