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:
@@ -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`).
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user