diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 453fd35..262982d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - "src/**" - "Cargo.toml" - "Cargo.lock" - - "ghp/**" + - "obk/**" - ".github/workflows/ci.yml" env: @@ -24,18 +24,18 @@ jobs: with: workspaces: | . - ghp + obk - name: cargo check run: cargo check - name: cargo clippy run: cargo clippy -- -D warnings - name: cargo test run: cargo test - # ghp is a separate crate (not a workspace member): the git credential + # obk is a separate crate (not a workspace member): the git credential # helper's fail-closed contract must be enforced in CI too. - - name: ghp clippy + - name: obk clippy run: cargo clippy --all-targets -- -D warnings - working-directory: ghp - - name: ghp test + working-directory: obk + - name: obk test run: cargo test - working-directory: ghp + working-directory: obk diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fab7fd7..48936e0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,16 +51,16 @@ jobs: - name: e2e — MCP reverse proxy against hosted endpoint env: GITHUB_TOKEN: ${{ steps.app.outputs.token }} - # App-backend mode section (ghpool mints its own tokens) + # App-backend mode section (octobroker mints its own tokens) APP_ID: ${{ secrets.APP_ID }} APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} run: ./scripts/e2e-mcp.sh - - name: Upload ghpool server log on failure + - name: Upload octobroker server log on failure if: failure() uses: actions/upload-artifact@v4 with: - name: ghpool-e2e-log + name: octobroker-e2e-log path: | - ghpool-e2e.log - ghpool-e2e-app.log + octobroker-e2e.log + octobroker-e2e-app.log if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e094f98..e9d89c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,17 +78,17 @@ jobs: - name: Build run: | cargo build --release --target ${{ matrix.target }} - cd ghp && cargo build --release --target ${{ matrix.target }} + cd obk && cargo build --release --target ${{ matrix.target }} - name: Package shell: bash env: VERSION: ${{ steps.tag.outputs.version }} run: | - NAME="ghpool-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}" + NAME="octobroker-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}" mkdir -p dist - cp target/${{ matrix.target }}/release/ghpool${{ matrix.ext }} dist/ - cp ghp/target/${{ matrix.target }}/release/ghp${{ matrix.ext }} dist/ + cp target/${{ matrix.target }}/release/octobroker${{ matrix.ext }} dist/ + cp obk/target/${{ matrix.target }}/release/obk${{ matrix.ext }} dist/ cp config.example.toml dist/ cd dist tar czf ../${NAME}.tar.gz * @@ -100,14 +100,14 @@ jobs: VERSION: ${{ steps.tag.outputs.version }} shell: bash run: | - NAME="ghpool-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" + NAME="octobroker-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" gh release upload "$TAG" "$NAME" --clobber - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: ghpool-${{ matrix.os }}-${{ matrix.arch }} - path: ghpool-*.tar.gz + name: octobroker-${{ matrix.os }}-${{ matrix.arch }} + path: octobroker-*.tar.gz retention-days: 7 docker: diff --git a/.gitignore b/.gitignore index 504b6d3..787e4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target config.toml -/ghp/target +/obk/target diff --git a/Cargo.lock b/Cargo.lock index 89d1df2..e8f3cd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,8 +952,8 @@ dependencies = [ ] [[package]] -name = "ghpool" -version = "0.3.4" +name = "octobroker" +version = "0.6.0" dependencies = [ "aws-config", "aws-sdk-secretsmanager", diff --git a/Cargo.toml b/Cargo.toml index 14c3ba1..f6c977b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "ghpool" +name = "octobroker" version = "0.6.0" edition = "2021" -description = "Internal GitHub API proxy with PAT pooling and caching" +description = "Secure GitHub gateway for AI agents — MCP, REST, and git push via short-lived, repo-scoped App tokens" license = "MIT" [dependencies] diff --git a/Dockerfile b/Dockerfile index 3fb2ece..e71fbee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,8 @@ RUN cargo build --release FROM debian:trixie-slim RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/ghpool /ghpool +COPY --from=builder /app/target/release/octobroker /octobroker EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -sf http://localhost:8080/healthz || exit 1 -ENTRYPOINT ["/ghpool"] +ENTRYPOINT ["/octobroker"] diff --git a/README.md b/README.md index 1dd0111..0a0ca11 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -# ghpool +

+ octobroker — Secure GitHub Gateway for AI Agents +

-A secure, cloud-native GitHub gateway for AI coding agents: agents get GitHub's official MCP tools and REST reads **without holding any GitHub credential** — ghpool authenticates each agent, enforces per-agent default-deny tool/repo policy, and injects short-lived, repo-scoped GitHub App tokens upstream. Read caching and PAT pooling included for high-throughput REST traffic. +A secure, cloud-native GitHub gateway for AI coding agents. Agents get GitHub's official MCP tools, REST/GraphQL reads, and even `git push` — **without holding a single GitHub credential**. octobroker authenticates each agent with a revocable key, enforces per-agent default-deny tool and repo policy, and mints short-lived, repo-scoped GitHub App tokens on demand — across multiple orgs from one endpoint. PAT pooling and read caching included for high-throughput REST traffic. -> Design: [docs/DESIGN.md](docs/DESIGN.md) · Onboarding: [docs/getting-started.md](docs/getting-started.md) · RFC history: [#15](https://github.com/openabdev/ghpool/issues/15) +> [octobroker.dev](https://octobroker.dev) · Design: [docs/DESIGN.md](docs/DESIGN.md) · Onboarding: [docs/getting-started.md](docs/getting-started.md) · RFC history: [#15](https://github.com/openabdev/octobroker/issues/15) ## Design Principles -- **No GitHub credential on the agent** — agents hold at most a ghpool API key (revocable, policy-bounded, not a GitHub credential). The GitHub credentials live in exactly one place: ghpool. -- **Default-deny policy engine** — each agent gets an exact tool allowlist and repository allowlist; new upstream tools are denied until explicitly granted. GitHub's own scoped installation tokens enforce the repo boundary independently of ghpool's parsing. +- **No GitHub credential on the agent** — agents hold at most an octobroker API key (revocable, policy-bounded, not a GitHub credential). The GitHub credentials live in exactly one place: octobroker. +- **Default-deny policy engine** — each agent gets an exact tool allowlist and repository allowlist; new upstream tools are denied until explicitly granted. GitHub's own scoped installation tokens enforce the repo boundary independently of octobroker's parsing. - **Short-lived credentials first** — GitHub App installation tokens (1h, auto-refreshed, repo-scoped at mint) are the recommended backend. Long-lived PAT pooling remains for REST read caching and legacy setups. - **Cloud-native** — runs on any Kubernetes (Amazon EKS, Google Cloud GKE, self-managed k8s) and Amazon ECS. Single static binary, no runtime dependencies. - **Secrets-first** — credentials are resolved at runtime from AWS Secrets Manager or Kubernetes secrets. No plain text tokens at rest. @@ -27,10 +29,10 @@ A secure, cloud-native GitHub gateway for AI coding agents: agents get GitHub's │ │ │ │ ▼ │ │ ┌──────────────────────────────────┐ │ -│ │ ghpool │ │ +│ │ octobroker │ │ │ │ │ │ │ │ ┌────────────────────────────┐ │ │ -│ │ │ Agent authn (X-Ghpool-Key) │ │ │ +│ │ │ Agent authn (X-Octobroker-Key) │ │ │ │ │ │ + default-deny policy │ │ ┌────────────────┐ │ │ │ │ tools / repos per agent │ │ │ Secrets Manager│ │ │ │ └────────────────────────────┘ │ │ (AWS/K8s/Env) │ │ @@ -71,7 +73,7 @@ Request Flow: POST /mcp (opt-in; read-only by default, writes behind a hard gate) → MCP Streamable HTTP reverse proxy to GitHub's hosted MCP server - → authenticate agent (X-Ghpool-Key) → default-deny tool/repo policy + → authenticate agent (X-Octobroker-Key) → default-deny tool/repo policy → inject scoped GitHub App token (or pooled PAT), pin per session → audit-log every tools/call; writes fail-closed audited ``` @@ -82,6 +84,7 @@ Request Flow: - Caches GitHub REST and GraphQL query responses in memory with configurable TTLs - Proxies GraphQL mutations with passthrough auth (client's own token, no caching) - **MCP reverse proxy** (opt-in) — agents connect an MCP client to `/mcp` and get GitHub's official MCP tools with **no GitHub credential on the agent**; per-agent keys + default-deny tool/repo allowlists, GitHub App credentials, and hard-gated audited writes +- **Git credential broker** (opt-in) — `obk` acts as a git credential helper: at `git push` time it exchanges the agent's octobroker key for a short-lived, single-repo, Contents-only GitHub App token — no stored git credential in the agent container - Mirrors the GitHub API path structure — clients just change the base URL - Restricts access to configured org/owner repos only - Auto-resolves GitHub username from tokens for audit logging @@ -107,9 +110,9 @@ curl http://localhost:8080/stats Config file search order: -1. `GHPOOL_CONFIG` env var (explicit path — always wins) +1. `OCTOBROKER_CONFIG` env var (explicit path — always wins) 2. `./config.toml` (current directory) -3. `$XDG_CONFIG_HOME/ghpool/config.toml` (defaults to `~/.config/ghpool/config.toml`) +3. `$XDG_CONFIG_HOME/octobroker/config.toml` (defaults to `~/.config/octobroker/config.toml`) 4. No file → environment variables only (see below) The loaded path is logged at startup. For configs in your home directory, prefer secret references (`env:`, `aws:secretsmanager:`, `k8s:`) over plain token literals. @@ -132,24 +135,24 @@ The `token` field in `[[identities]]` supports multiple secret sources, so crede Store PATs as a JSON object in a single secret: ```sh -aws secretsmanager create-secret --name ghpool/pats \ +aws secretsmanager create-secret --name octobroker/pats \ --secret-string '{"pat_alice":"ghp_xxx","pat_bob":"ghp_yyy"}' ``` ```toml [[identities]] id = "alice" -token = "aws:secretsmanager:ghpool/pats:pat_alice" +token = "aws:secretsmanager:octobroker/pats:pat_alice" ``` -ghpool uses the standard AWS credential chain (instance profile, ECS task role, SSO, env vars). +octobroker uses the standard AWS credential chain (instance profile, ECS task role, SSO, env vars). #### Google Cloud Secret Manager (planned) ```toml [[identities]] id = "alice" -token = "gcp:secretmanager:projects/my-proj/secrets/ghpool-pat:latest" +token = "gcp:secretmanager:projects/my-proj/secrets/octobroker-pat:latest" ``` GCP support is on the roadmap. Contributions welcome. @@ -163,7 +166,7 @@ Mount your secret as a volume at `/etc/secrets/` and reference it: apiVersion: v1 kind: Secret metadata: - name: ghpool-pats + name: octobroker-pats namespace: default stringData: pat_alice: ghp_xxx @@ -172,7 +175,7 @@ stringData: ```toml [[identities]] id = "alice" -token = "k8s:default/ghpool-pats:pat_alice" +token = "k8s:default/octobroker-pats:pat_alice" ``` Works with any Kubernetes distribution — EKS, GKE, AKS, k3s, or self-managed. @@ -180,35 +183,35 @@ Works with any Kubernetes distribution — EKS, GKE, AKS, k3s, or self-managed. ### Environment variables only ```sh -export GHPOOL_PORT=8080 -export GHPOOL_ALLOWED_OWNERS=openclaw,openabdev -export GHPOOL_PAT_ALICE=ghp_xxx -export GHPOOL_PAT_BOB=ghp_yyy +export OCTOBROKER_PORT=8080 +export OCTOBROKER_ALLOWED_OWNERS=openclaw,openabdev +export OCTOBROKER_PAT_ALICE=ghp_xxx +export OCTOBROKER_PAT_BOB=ghp_yyy ``` -PATs are discovered from any env var matching `GHPOOL_PAT_=`. +PATs are discovered from any env var matching `OCTOBROKER_PAT_=`. ## Deployment ### Docker ```sh -docker build -t ghpool . -docker run -p 8080:8080 -v ./config.toml:/config.toml ghpool +docker build -t octobroker . +docker run -p 8080:8080 -v ./config.toml:/config.toml octobroker ``` ### ECS (Service Connect) Deploy as a service in your ECS cluster with Cloud Map namespace. Other services access it via: ``` -http://ghpool.:8080/repos/owner/repo/pulls/123 +http://octobroker.:8080/repos/owner/repo/pulls/123 ``` ### Kubernetes Deploy as a ClusterIP Service. Other pods access it via: ``` -http://ghpool..svc.cluster.local:8080/repos/owner/repo/pulls/123 +http://octobroker..svc.cluster.local:8080/repos/owner/repo/pulls/123 ``` ## API @@ -230,7 +233,7 @@ POST /graphql - **Queries** — routed through pooled PATs, responses cached - **Mutations** — client's own `Authorization` header passed through to GitHub (no pooling, no caching) -If a mutation request has no `Authorization` header, ghpool returns `401`. +If a mutation request has no `Authorization` header, octobroker returns `401`. ``` ┌────────────────────────────────────────────────────────────────┐ @@ -266,15 +269,15 @@ If a mutation request has no `Authorization` header, ghpool returns `401`. ### MCP (POST/GET/DELETE /mcp) — opt-in -Reverse proxy to [GitHub's hosted MCP server](https://github.com/github/github-mcp-server): agents connect a Model Context Protocol client to ghpool and get GitHub's official MCP tools — **with no GitHub credential on the agent**. ghpool strips any client `Authorization` header and injects a pooled credential upstream. +Reverse proxy to [GitHub's hosted MCP server](https://github.com/github/github-mcp-server): agents connect a Model Context Protocol client to octobroker and get GitHub's official MCP tools — **with no GitHub credential on the agent**. octobroker strips any client `Authorization` header and injects a pooled credential upstream. -> **Status: writes available behind a hard gate.** Read-only by default; `enable_writes` unlocks write tools for authenticated agents only, and requires the GitHub App backend plus fail-closed audit (validated at startup). See the [RFC](https://github.com/openabdev/ghpool/issues/15). +> **Status: writes available behind a hard gate.** Read-only by default; `enable_writes` unlocks write tools for authenticated agents only, and requires the GitHub App backend plus fail-closed audit (validated at startup). See the [RFC](https://github.com/openabdev/octobroker/issues/15). ``` ┌───────────────── Private Network / VPC ─────────────────┐ │ │ │ ┌───────────────────┐ ┌────────────────────┐ │ -│ │ MCP client │ │ ghpool │ │ +│ │ MCP client │ │ octobroker │ │ │ │ (agent) │ MCP │ │ │ │ │ │ ──────► │ 1. strip client │ │ │ │ no GitHub │ HTTP │ Authorization │ │ @@ -293,7 +296,7 @@ Reverse proxy to [GitHub's hosted MCP server](https://github.com/github/github-m └──────────────────────────────┘ ``` -Enable it in `config.toml` (or `GHPOOL_MCP_ENABLED=true`): +Enable it in `config.toml` (or `OCTOBROKER_MCP_ENABLED=true`): ```toml [mcp] @@ -308,7 +311,7 @@ Behavior notes: - **Sessions are pinned** — the pooled identity is selected at `initialize` and reused for the whole MCP session. An unknown or expired session gets `404` (per MCP spec) and the client re-initializes transparently. - **Tool names differ from the write server** — the readonly surface uses e.g. `issue_read`, not `get_issue`. Discover them via `tools/list`. - **Audit log** — every JSON-RPC frame is logged with method, tool name, identity, and session: `MCP tools/call issue_read [via alice] [session=7b86a7eb]`. -- **`allowed_owners` is not enforced on `/mcp`** in Phase 1 — access is bounded by the pooled credential's own permissions and the read-only upstream. Per-agent repo allowlists arrive in Phase 2. +- **`allowed_owners` is not enforced on `/mcp`** — without `[[mcp.agents]]` configured, access is bounded only by the pooled credential's own permissions and the read-only upstream. For repo-level control, configure per-agent `repos` allowlists (below). #### Per-agent authentication (Phase 2a) @@ -317,15 +320,15 @@ Add `[[mcp.agents]]` entries to require an API key on every `/mcp` request and e ```toml [[mcp.agents]] id = "openab-bot" -key = "aws:secretsmanager:ghpool/mcp-keys:openab" # env:/k8s: refs also work +key = "aws:secretsmanager:octobroker/mcp-keys:openab" # env:/k8s: refs also work tools = ["issue_read", "list_issues", "pull_request_read"] ``` -- With any agent configured, requests without a valid `X-Ghpool-Key` get `401`; a `tools/call` for a tool not on the agent's allowlist gets `403` at the proxy — it never reaches GitHub. The allowlist is also injected upstream as `X-MCP-Tools`, so `tools/list` natively shows the agent only its permitted tools. +- With any agent configured, requests without a valid `X-Octobroker-Key` get `401`; a `tools/call` for a tool not on the agent's allowlist gets `403` at the proxy — it never reaches GitHub. The allowlist is also injected upstream as `X-MCP-Tools`, so `tools/list` natively shows the agent only its permitted tools. - New upstream tools are **denied by default** until added to an agent's `tools` list. -- The key is a **ghpool credential, not a GitHub credential** — a leak is bounded by that agent's allowlist and revoked by editing ghpool config, without touching GitHub. +- The key is a **octobroker credential, not a GitHub credential** — a leak is bounded by that agent's allowlist and revoked by editing octobroker config, without touching GitHub. - Audit lines include the agent: `MCP tools/call issue_read [agent=openab-bot via alice] [session=…]`. -- Terminate TLS in front of ghpool (ALB, ingress, mesh) in production — the key travels in a header. +- Terminate TLS in front of octobroker (ALB, ingress, mesh) in production — the key travels in a header. Client config gains one line: @@ -333,14 +336,14 @@ Client config gains one line: { "mcpServers": { "github": { - "url": "http://ghpool.:8080/mcp", - "headers": { "X-Ghpool-Key": "${GHPOOL_KEY}" } + "url": "http://octobroker.:8080/mcp", + "headers": { "X-Octobroker-Key": "${OCTOBROKER_KEY}" } } } } ``` -Deliver `GHPOOL_KEY` to the agent container via ECS task secrets / K8s Secrets — most MCP clients expand `${ENV}` in config. +Deliver `OCTOBROKER_KEY` to the agent container via ECS task secrets / K8s Secrets — most MCP clients expand `${ENV}` in config. #### Write access (Phase 2b) @@ -352,59 +355,59 @@ enable_writes = true # startup FAILS unless all three sections below ex [mcp.github_app] # writes never run on pooled PATs app_id = "123456" -private_key = "aws:secretsmanager:ghpool/app:private_key" +private_key = "aws:secretsmanager:octobroker/app:private_key" owner = "openabdev" [mcp.audit] # writes are fail-closed audited -path = "/var/lib/ghpool/mcp-audit.jsonl" +path = "/var/lib/octobroker/mcp-audit.jsonl" [[mcp.agents]] # writes are only for authenticated agents id = "openab-bot" -key = "aws:secretsmanager:ghpool/mcp-keys:openab" -tools = ["issue_read", "create_issue", "add_issue_comment", "ghpool_review_minimize_comment"] -repos = ["openabdev/ghpool", "openabdev/openab"] +key = "aws:secretsmanager:octobroker/mcp-keys:openab" +tools = ["issue_read", "create_issue", "add_issue_comment", "octobroker_review_minimize_comment"] +repos = ["openabdev/octobroker", "openabdev/openab"] ``` How the write path is bounded: - **Default-deny stack**: agent key → tool allowlist → write classification → repo allowlist (deny-if-unresolvable) → per-agent in-flight cap → fail-closed audit record → forward. -- **Scoped credentials**: when an agent's `repos` are all exact entries under one owner, its installation token is minted with the API's `repositories` parameter — **GitHub itself enforces the repo boundary**, independent of ghpool's argument parsing. Wildcard or mixed-owner allowlists fall back to an installation-wide token (proxy-side checks still apply). +- **Scoped credentials**: when an agent's `repos` are all exact entries under one owner, its installation token is minted with the API's `repositories` parameter — **GitHub itself enforces the repo boundary**, independent of octobroker's argument parsing. Wildcard or mixed-owner allowlists fall back to an installation-wide token (proxy-side checks still apply). - **Audit**: two fsync'd JSONL records per write (pre-flight + result). The result captures the MCP tool outcome (`result.isError`) — HTTP 200 alone is not treated as success. If the pre-flight record cannot be persisted, the write is rejected (503) without reaching GitHub. Argument values are never recorded. -- **No auto-retry**: ghpool never retries a forwarded call; an ambiguous write outcome (e.g. connection lost mid-response) is recorded as undeterminable and surfaced to the client — retry decisions belong to the caller. -- **Revocation**: session pins live in process memory; key rotation uses dual `keys`, and any config change (agent disabled, policy tightened) takes effect by restart, which clears all sessions. Upstream session DELETE is a no-op at GitHub — ghpool's pin cache is the session authority. +- **No auto-retry**: octobroker never retries a forwarded call; an ambiguous write outcome (e.g. connection lost mid-response) is recorded as undeterminable and surfaced to the client — retry decisions belong to the caller. +- **Revocation**: session pins live in process memory; key rotation uses dual `keys`, and any config change (agent disabled, policy tightened) takes effect by restart, which clears all sessions. Upstream session DELETE is a no-op at GitHub — octobroker's pin cache is the session authority. Required GitHub App permissions (grant only what your agents' tools need): | Tools | App permission | |-------|----------------| | `issue_read`, `list_issues`, `create_issue`, `add_issue_comment` | Issues: read / write | -| `ghpool_review_minimize_comment` | Issues: write and Pull requests: write | +| `octobroker_review_minimize_comment` | Issues: write and Pull requests: write | | `pull_request_read`, `create_pull_request`, `merge_pull_request` | Pull requests: read / write | | `get_file_contents`, `create_or_update_file`, `push_files` | Contents: read / write | | `list_workflows`, `run_workflow` | Actions: read / write | -#### ghpool-owned review tools (issue #44 MVP) +#### octobroker-owned review tools (issue #44 MVP) When writes are enabled and an authenticated agent explicitly includes the tool in its allowlist, `tools/list` also advertises it. For example: ```toml [[mcp.agents]] id = "review-bot" -key = "env:GHPOOL_REVIEW_KEY" -tools = ["issue_read", "list_issues", "ghpool_review_minimize_comment"] -repos = ["openabdev/ghpool"] +key = "env:OCTOBROKER_REVIEW_KEY" +tools = ["issue_read", "list_issues", "octobroker_review_minimize_comment"] +repos = ["openabdev/octobroker"] ``` -The tool accepts `owner`, `repo`, `node_id`, and a `classifier` (`ABUSE`, `DUPLICATE`, `OFF_TOPIC`, `OUTDATED`, `RESOLVED`, or `SPAM`). It supports issue and pull-request comments authored by the current GitHub App bot identity; it does not minimize human-authored comments or unsupported node types. Before mutating, it verifies both the App-bot author and the exact repository owner/name against the policy arguments, then executes GitHub's `minimizeComment` GraphQL mutation locally through ghpool's scoped GitHub App credential. The operation is not forwarded to the upstream MCP server. The call uses the same repository policy, write gate, in-flight limit, and fail-closed audit as upstream write tools. Agents that do not explicitly allowlist the name, or that have writes disabled, neither see it in `tools/list` nor can call it. +The tool accepts `owner`, `repo`, `node_id`, and a `classifier` (`ABUSE`, `DUPLICATE`, `OFF_TOPIC`, `OUTDATED`, `RESOLVED`, or `SPAM`). It supports issue and pull-request comments authored by the current GitHub App bot identity; it does not minimize human-authored comments or unsupported node types. Before mutating, it verifies both the App-bot author and the exact repository owner/name against the policy arguments, then executes GitHub's `minimizeComment` GraphQL mutation locally through octobroker's scoped GitHub App credential. The operation is not forwarded to the upstream MCP server. The call uses the same repository policy, write gate, in-flight limit, and fail-closed audit as upstream write tools. Agents that do not explicitly allowlist the name, or that have writes disabled, neither see it in `tools/list` nor can call it. -This is intentionally a narrow MVP: ghpool does not expose arbitrary GraphQL, and upstream GitHub MCP tools remain unchanged. Any future ghpool-owned tool must preserve the same explicit allowlist, repository binding, write gate, and audit requirements. +This is intentionally a narrow MVP: octobroker does not expose arbitrary GraphQL, and upstream GitHub MCP tools remain unchanged. Any future octobroker-owned tool must preserve the same explicit allowlist, repository binding, write gate, and audit requirements. -The tool surface returned by `tools/list` shrinks to match the App's actual permissions (verified in the [#22 spike](https://github.com/openabdev/ghpool/issues/22)) — grant conservatively and expand as agents need more. +The tool surface returned by `tools/list` shrinks to match the App's actual permissions (verified in the [#22 spike](https://github.com/openabdev/octobroker/issues/22)) — grant conservatively and expand as agents need more. #### Multi-installation routing (one key, many orgs) -One agent, one `X-Ghpool-Key`, one MCP server entry — repositories in several +One agent, one `X-Octobroker-Key`, one MCP server entry — repositories in several organizations. Replace the singular `[mcp.github_app]` with one `[[mcp.github_apps]]` entry per installation (the same App installed in each org, or one App per org): @@ -412,19 +415,19 @@ org, or one App per org): ```toml [[mcp.github_apps]] app_id = "123456" -private_key = "aws:secretsmanager:ghpool/app:private_key" +private_key = "aws:secretsmanager:octobroker/app:private_key" owner = "openabdev" # routing key — unique per entry installation_id = 11111111 # recommended: skip discovery [[mcp.github_apps]] app_id = "123456" -private_key = "aws:secretsmanager:ghpool/app:private_key" +private_key = "aws:secretsmanager:octobroker/app:private_key" owner = "oablab" installation_id = 22222222 [[mcp.agents]] id = "b0" -key = "aws:secretsmanager:ghpool/mcp-keys:b0" +key = "aws:secretsmanager:octobroker/mcp-keys:b0" tools = ["issue_read", "list_issues", "create_issue", "add_issue_comment"] repos = ["openabdev/openab", "oablab/chi"] # owners select the installations ``` @@ -450,7 +453,7 @@ tools/call {owner: "oablab", …} → session B (oablab token) any upstream sessions already opened are cleaned up (best-effort DELETE). Tokens are never mixed within one upstream session, preserving the pinning invariant per installation. -- **One downstream session** — the client sees a single session ID; ghpool +- **One downstream session** — the client sees a single session ID; octobroker maps it to the per-installation upstream sessions. `DELETE` and `notifications/*` fan out to every route (best-effort for secondaries). When a pinned token expires the session gets 404 and the client @@ -481,7 +484,7 @@ per-org blast-radius isolation. MCP covers issues and PRs, but `git push` speaks the Git protocol — it needs a real credential at push time. `enable_git_credentials` lets a -repository-scoped agent exchange its ghpool key for a **short-lived, +repository-scoped agent exchange its octobroker key for a **short-lived, single-repo** App installation token, eliminating the last long-lived GitHub credential in the agent container: @@ -496,22 +499,22 @@ the singular `[mcp.github_app]` form, `owner` is **required** when git credentials are enabled — the explicit `installation_id` is verified against the installation's actual account before any token is issued. -Agent side, `ghp` doubles as a standard git credential helper. Register it +Agent side, `obk` doubles as a standard git credential helper. Register it as the **only** helper for `github.com` — `--replace-all` with an empty first entry clears any inherited helpers (osxkeychain, GCM, `gh auth git-credential`) that would otherwise supply broader credentials: ```sh git config --global --replace-all credential."https://github.com".helper "" -git config --global --add credential."https://github.com".helper "!ghp git-credential" +git config --global --add credential."https://github.com".helper "!obk git-credential" git config --global credential."https://github.com".useHttpPath true ``` Every `git push` then flows: ``` -git push → ghp git-credential (GHPOOL_KEY from env) - → GET /git-credential?repo=owner/name (X-Ghpool-Key) +git push → obk git-credential (OCTOBROKER_KEY from env) + → GET /git-credential?repo=owner/name (X-Octobroker-Key) → key auth → repo allowlist → installation routing → durable audit preflight (phase: git_credential_request) → installation owner verified against GitHub @@ -537,8 +540,8 @@ Properties: - **Deny-by-default** — repo-less agents, off-allowlist repos, and owners without an installation are refused before any mint. - **Fail-closed helper** — once a request is recognized as `github.com` - HTTPS, any failure (missing `GHPOOL_KEY`, missing path, policy denial, - network error) makes `ghp` emit `quit=true`, telling git to stop the + HTTPS, any failure (missing `OCTOBROKER_KEY`, missing path, policy denial, + network error) makes `obk` emit `quit=true`, telling git to stop the helper cascade instead of falling through to broader stored credentials or prompting. Non-GitHub hosts are declined quietly so other helpers can serve them. `gist.github.com` is not supported. @@ -549,7 +552,7 @@ Deployment notes: - Requires egress to `api.githubcopilot.com` (the only additional external dependency). - Run a **single replica** while MCP is enabled — session pins live in process memory. A rolling deploy terminates sessions; clients recover by re-initializing. -- Inside a trusted network, any workload that can reach `/mcp` gets the same read-only access (same trust model as ghpool's REST reads). Put TLS and agent authentication in front before any write-capable phase. +- Inside a trusted network, any workload that can reach `/mcp` gets the same read-only access (same trust model as octobroker's REST reads). Put TLS and agent authentication in front before any write-capable phase. - If the hosted endpoint is unreachable from your network, point `upstream` at a self-hosted [`github-mcp-server`](https://github.com/github/github-mcp-server) instead — same protocol and headers. ### Management @@ -561,30 +564,30 @@ Deployment notes: ## How clients use it -### ghp CLI (recommended) +### obk CLI (recommended) -`ghp` is a drop-in `gh` shim that routes read commands through ghpool's REST API (pooled + cached) and falls through to the real `gh` for writes. +`obk` is a drop-in `gh` shim that routes read commands through octobroker's REST API (pooled + cached) and falls through to the real `gh` for writes. ```sh -export GHPOOL_URL=http://ghpool.openab.local:8080 +export OCTOBROKER_URL=http://octobroker.openab.local:8080 -# Reads — through ghpool (pooled + cached) -ghp api repos/org/repo --jq .stargazers_count -ghp issue list -R org/repo -L 10 -ghp pr list -R org/repo -ghp pr view 123 -R org/repo -ghp run list -R org/repo +# Reads — through octobroker (pooled + cached) +obk api repos/org/repo --jq .stargazers_count +obk issue list -R org/repo -L 10 +obk pr list -R org/repo +obk pr view 123 -R org/repo +obk run list -R org/repo # Writes — falls through to real gh (direct to GitHub) -ghp issue create -R org/repo -t "title" -b "body" -ghp issue comment 123 -R org/repo -b "comment" -ghp pr create -R org/repo -t "title" -b "body" +obk issue create -R org/repo -t "title" -b "body" +obk issue comment 123 -R org/repo -b "comment" +obk pr create -R org/repo -t "title" -b "body" ``` To replace `gh` transparently: ```sh -ln -sf $(which ghp) ~/bin/gh +ln -sf $(which obk) ~/bin/gh export PATH=~/bin:$PATH ``` @@ -594,11 +597,11 @@ export PATH=~/bin:$PATH export GITHUB_API_URL=http://localhost:8080 ``` -REST calls (`gh api repos/...`) route through ghpool. Note: `gh` CLI's built-in commands (`gh issue list`, `gh pr list`) use GraphQL internally and bypass `GITHUB_API_URL` — use `ghp` for full coverage. +REST calls (`gh api repos/...`) route through octobroker. Note: `gh` CLI's built-in commands (`gh issue list`, `gh pr list`) use GraphQL internally and bypass `GITHUB_API_URL` — use `obk` for full coverage. ### Coding agents -Set the GitHub API base URL to point at ghpool: +Set the GitHub API base URL to point at octobroker: ```sh export GITHUB_API_BASE=http://localhost:8080 @@ -606,7 +609,7 @@ export GITHUB_API_BASE=http://localhost:8080 ### MCP clients (agents) -Point any Streamable-HTTP MCP client at ghpool — no GitHub token, no `gh` CLI, no git credentials in the agent container. +Point any Streamable-HTTP MCP client at octobroker — no GitHub token, no `gh` CLI, no git credentials in the agent container. Kiro CLI (`~/.kiro/settings/mcp.json`) and most JSON-configured clients: @@ -614,29 +617,32 @@ Kiro CLI (`~/.kiro/settings/mcp.json`) and most JSON-configured clients: { "mcpServers": { "github": { - "url": "http://ghpool.:8080/mcp" + "url": "http://octobroker.:8080/mcp", + "headers": { "X-Octobroker-Key": "${OCTOBROKER_KEY}" } } } } ``` +With `[[mcp.agents]]` configured (recommended), every request needs the agent's `X-Octobroker-Key` — deliver it via ECS task secrets / K8s Secrets. Without any agents configured (network-trust mode), the `headers` line can be dropped. + Claude Code: ```sh -claude mcp add --transport http github http://ghpool.:8080/mcp +claude mcp add --transport http github http://octobroker.:8080/mcp \ + --header "X-Octobroker-Key: ${OCTOBROKER_KEY}" ``` -Verify from the container (no `Authorization` header anywhere): +Verify from the container (no GitHub credential anywhere): ```sh -curl -s -X POST http://ghpool:8080/mcp \ +curl -s -X POST http://octobroker:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ + -H "X-Octobroker-Key: $OCTOBROKER_KEY" \ -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' -i | grep -i mcp-session-id ``` -In Phase 2, agents will additionally present a ghpool API key (`X-Ghpool-Key` header from an env-injected secret) mapped to a per-agent tool/repo allowlist — see [#17](https://github.com/openabdev/ghpool/issues/17). - ### Direct curl ```sh diff --git a/config.example.toml b/config.example.toml index 134ee0e..9bc3254 100644 --- a/config.example.toml +++ b/config.example.toml @@ -6,7 +6,7 @@ allowed_owners = ["openclaw", "openabdev"] # "ghp_xxx" — plain literal # "env:GITHUB_TOKEN" — from environment variable # "aws:secretsmanager:my-secret:github_pat" — from AWS Secrets Manager (JSON key) -# "k8s:default/ghpool-secrets:pat" — from K8s secret mounted at /etc/secrets/ +# "k8s:default/octobroker-secrets:pat" — from K8s secret mounted at /etc/secrets/ [[identities]] id = "local" @@ -14,11 +14,11 @@ token = "env:GITHUB_TOKEN" # [[identities]] # id = "prod" -# token = "aws:secretsmanager:ghpool/pats:pat_prod" +# token = "aws:secretsmanager:octobroker/pats:pat_prod" # [[identities]] # id = "k8s" -# token = "k8s:default/ghpool-secrets:pat" +# token = "k8s:default/octobroker-secrets:pat" [cache] max_entries = 10000 @@ -34,11 +34,11 @@ default_ttl_secs = 60 raw_ttl_secs = 30 raw_max_bytes = 268435456 # 256 MiB — total byte budget, enforced via weigher -# MCP reverse proxy (Phase 1: read-only) — https://github.com/openabdev/ghpool/issues/15 -# When enabled, ghpool proxies MCP Streamable HTTP traffic on /mcp to GitHub's +# MCP reverse proxy (Phase 1: read-only) — https://github.com/openabdev/octobroker/issues/15 +# When enabled, octobroker proxies MCP Streamable HTTP traffic on /mcp to GitHub's # hosted MCP server, injecting a pooled credential upstream. Agents connect # with zero GitHub tokens: -# { "url": "http://ghpool:8080/mcp" } +# { "url": "http://octobroker:8080/mcp" } [mcp] enabled = false # Enable WRITE tools for authenticated agents (Phase 2b-5). Startup fails @@ -48,7 +48,7 @@ enabled = false # switches to the full write-capable surface. # enable_writes = false # Enable GET /git-credential: repository-scoped agents exchange their -# X-Ghpool-Key for a short-lived GitHub App installation token scoped to +# X-Octobroker-Key for a short-lived GitHub App installation token scoped to # EXACTLY ONE repository with Contents-only permissions, usable as a # git-over-HTTPS credential (username `x-access-token`). Closes the # long-lived-PAT gap for `git push`. Same hard gate as writes: agents + @@ -56,7 +56,7 @@ enabled = false # token is returned). The App needs Contents: read/write for pushes. With # the singular [mcp.github_app] form, `owner` is REQUIRED — the explicit # installation_id is verified against the installation's actual account -# before issuance. Client side: `ghp git-credential` is a drop-in git +# before issuance. Client side: `obk git-credential` is a drop-in git # credential helper (see README for the fail-closed helper setup). # enable_git_credentials = false # Max concurrent write calls per agent (0 = unlimited). @@ -77,7 +77,7 @@ enabled = false # record captures the MCP tool outcome (result.isError) — HTTP 200 alone is # not a success signal. Argument VALUES are never recorded (key names only). # [mcp.audit] -# path = "/var/lib/ghpool/mcp-audit.jsonl" +# path = "/var/lib/octobroker/mcp-audit.jsonl" # max_result_bytes = 4194304 # response buffer cap for outcome extraction # GitHub App credential backend (Phase 2b) — RECOMMENDED for production MCP. @@ -88,7 +88,7 @@ enabled = false # The PAT pool remains for REST/GraphQL and as the MCP fallback. # [mcp.github_app] # app_id = "123456" -# private_key = "aws:secretsmanager:ghpool/app:private_key" # env:/k8s: also work +# private_key = "aws:secretsmanager:octobroker/app:private_key" # env:/k8s: also work # owner = "openabdev" # installation discovered from org/user… # # installation_id = 87654321 # …or set explicitly @@ -97,26 +97,26 @@ enabled = false # installation, keyed by its owner. tools/call frames route to the # installation matching the repository owner resolved from the CALL # ARGUMENTS — never from anything the agent chooses directly. At -# `initialize`, ghpool eagerly mints one repo-scoped token and opens one +# `initialize`, octobroker eagerly mints one repo-scoped token and opens one # upstream MCP session per owner in the agent's repo allowlist (fail-closed: # any failure fails the whole initialize); the client sees a single session. # Requires [[mcp.agents]] with non-empty exact `repos` — every repo owner # must be covered by an entry here (validated at startup). # [[mcp.github_apps]] # app_id = "123456" # the same App installed in both orgs, or… -# private_key = "aws:secretsmanager:ghpool/app:private_key" +# private_key = "aws:secretsmanager:octobroker/app:private_key" # owner = "openabdev" # routing key (unique across entries) # # installation_id = 87654321 # recommended: skip discovery # # [[mcp.github_apps]] # app_id = "654321" # …a separate App per org -# private_key = "aws:secretsmanager:ghpool/app-oablab:private_key" +# private_key = "aws:secretsmanager:octobroker/app-oablab:private_key" # owner = "oablab" # # installation_id = 12345678 # Per-agent authentication + default-deny tool allowlists (Phase 2a). # When any [[mcp.agents]] entry exists, EVERY /mcp request must present a -# valid X-Ghpool-Key header. tools/call for a tool not on the agent's +# valid X-Octobroker-Key header. tools/call for a tool not on the agent's # allowlist is rejected at the proxy (403); the allowlist is also injected # upstream as X-MCP-Tools. No entries = Phase 1 network-trust mode. # @@ -127,16 +127,16 @@ enabled = false # presented by a different agent is rejected (403). # [[mcp.agents]] # id = "openab-bot" -# key = "aws:secretsmanager:ghpool/mcp-keys:openab" # or env:GHPOOL_KEY_OPENAB -# # keys = ["env:GHPOOL_KEY_OPENAB", "env:GHPOOL_KEY_OPENAB_NEXT"] # rotation +# key = "aws:secretsmanager:octobroker/mcp-keys:openab" # or env:OCTOBROKER_KEY_OPENAB +# # keys = ["env:OCTOBROKER_KEY_OPENAB", "env:OCTOBROKER_KEY_OPENAB_NEXT"] # rotation # tools = ["issue_read", "list_issues", "pull_request_read"] # # Repository allowlist: "owner/repo" exact or "owner/*" wildcard. # # Non-empty = every tools/call must resolve to a listed repo from its # # arguments; calls with no repo target (e.g. search_code, get_me) are # # DENIED. Empty/omitted = no repository restriction. -# repos = ["openabdev/ghpool", "oablab/*"] +# repos = ["openabdev/octobroker", "oablab/*"] # # [[mcp.agents]] # id = "ci-agent" -# key = "env:GHPOOL_KEY_CI" +# key = "env:OCTOBROKER_KEY_CI" # tools = ["get_file_contents", "list_commits"] diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 8593ed3..897c013 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,7 +1,7 @@ -# ghpool Design +# octobroker Design -> Living distillation of [RFC #15](https://github.com/openabdev/ghpool/issues/15) -> (Revision 2) and the [Phase 0 spike findings](https://github.com/openabdev/ghpool/issues/22), +> Living distillation of [RFC #15](https://github.com/openabdev/octobroker/issues/15) +> (Revision 2) and the [Phase 0 spike findings](https://github.com/openabdev/octobroker/issues/22), > as actually shipped. For onboarding, see [getting-started.md](getting-started.md). ## Problem @@ -15,19 +15,19 @@ GitHub credential inside the agent's environment: | Token vending machine | Agent still holds a live token for its lifetime; misusable within scope | | Per-agent fine-grained PATs | Management burden; still long-lived; single-org | -ghpool's position: **the agent never holds any GitHub credential.** It holds -at most a ghpool API key — revocable centrally, bounded by policy, useless +octobroker's position: **the agent never holds any GitHub credential.** It holds +at most an octobroker API key — revocable centrally, bounded by policy, useless against GitHub directly. ## Architecture -ghpool is a **credential-swapping reverse proxy with a default-deny policy +octobroker is a **credential-swapping reverse proxy with a default-deny policy engine**, sitting between agents and two GitHub surfaces: - **MCP** (`/mcp`) -> GitHub's hosted MCP server (`api.githubcopilot.com/mcp/`), with a - narrowly scoped ghpool-owned review-tool exception. Upstream schemas remain - proxied verbatim; `ghpool_*` tools are explicit, default-deny, App-backed, - repository-bound, and fail-closed audited. ghpool does not expose arbitrary + narrowly scoped octobroker-owned review-tool exception. Upstream schemas remain + proxied verbatim; `octobroker_*` tools are explicit, default-deny, App-backed, + repository-bound, and fail-closed audited. octobroker does not expose arbitrary GraphQL or a general custom-tool registry. - **REST/GraphQL** (`/{path}`, `/graphql`) → `api.github.com`, with PAT pooling (budget-aware selection) and in-memory read caching. GraphQL @@ -36,7 +36,7 @@ engine**, sitting between agents and two GitHub surfaces: ### Request path (MCP) ``` -agent → [authn: X-Ghpool-Key] → [session binding] → [tool allowlist] +agent → [authn: X-Octobroker-Key] → [session binding] → [tool allowlist] → [write classification] → [repo allowlist (deny-if-unresolvable)] → [in-flight cap] → [fail-closed audit] → forward with scoped token → [buffer+parse write outcomes] → audit result @@ -51,11 +51,11 @@ remains for REST reads and legacy setups). Decided in Phase 0 after the PAT-pooling compliance concern (GitHub ToS §H rate-limit aggregation) was raised in review: -- Minted by ghpool from the App private key (RS256 JWT → installation +- Minted by octobroker from the App private key (RS256 JWT → installation token), cached, auto-refreshed 5 minutes before the 1-hour expiry. - **Scoped at mint**: an agent whose repo allowlist is exact entries under one owner gets tokens minted with the API's `repositories` parameter — - GitHub itself enforces the repo boundary, independent of ghpool's + GitHub itself enforces the repo boundary, independent of octobroker's argument parsing (which remains as defense-in-depth). One credential per policy envelope. - **Writes never run on PATs** — enforced by startup validation, not @@ -72,7 +72,7 @@ Sessions (`Mcp-Session-Id`) are pinned to `(credential, agent)` at 403 (binding violation). - A session cannot outlive its credential: expired App-token pins terminate the session. Provider refreshes don't disturb in-flight sessions. -- **ghpool's pin cache is the sole session authority.** Phase 0 measured the +- **octobroker's pin cache is the sole session authority.** Phase 0 measured the hosted endpoint's own session semantics as fail-open: upstream DELETE is a no-op (the session remains usable), and unknown sessions get 400 not 404. Nothing about session validity is delegated upstream. @@ -117,7 +117,7 @@ before the call is forwarded; if it cannot be persisted, the write is rejected (503) without side effects. The result record captures the **MCP tool outcome** — `result.isError` arrives inside HTTP 200/SSE, so transport status alone is never treated as success. Argument values are never logged -(key names + resolved repo only). ghpool never auto-retries a forwarded +(key names + resolved repo only). octobroker never auto-retries a forwarded call; ambiguous outcomes are recorded as undeterminable and surfaced to the caller. @@ -128,7 +128,7 @@ caller. - **No rate-limit headers on the hosted MCP endpoint** (Phase 0 finding) — budget accounting stays REST-driven; per-agent quotas are Phase 3. - **GitHub-side write attribution is the App identity**, not the individual - agent. The ghpool audit log is the per-agent ledger; GraphQL mutation + agent. The octobroker audit log is the per-agent ledger; GraphQL mutation passthrough remains the right path when GitHub-side per-human attribution matters. - **Contract-drift risk**: the hosted MCP surface (tool names, headers like @@ -145,9 +145,9 @@ caller. | Proxy official schemas, define no tools | RFC Rev 1 | Zero schema maintenance; GitHub owns the surface | | GitHub App primary, PAT pool legacy | RFC Rev 2 / #22 | ToS compliance, short-lived creds, scoped mint | | `X-MCP-Tools` not `X-MCP-Toolsets` | #22 finding F | Toolsets fail open on invalid names | -| ghpool is session authority | #22 finding I | Upstream DELETE is a no-op | +| octobroker is session authority | #22 finding I | Upstream DELETE is a no-op | | 404 on unknown session, never rotate identity | #20 review | MCP spec; no silent actor switching | | Buffer + parse write responses | #17 review | `isError` inside HTTP 200; audit must record tool outcome | | Scoped installation tokens per policy envelope | #17 review | GitHub enforces the repo boundary, not just our parser | | Writes: App + audit + agents required in code | #17 review | Hard rules, not documented hopes | -| `ghpool_*` review tools as a narrow MCP exception | #44 / PR #45 | Fill upstream capability gaps without arbitrary GraphQL; preserve default-deny, repo binding, App credentials, and audit | +| `octobroker_*` review tools as a narrow MCP exception | #44 / PR #45 | Fill upstream capability gaps without arbitrary GraphQL; preserve default-deny, repo binding, App credentials, and audit | diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..32b44d2 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/getting-started.md b/docs/getting-started.md index 4e1f7de..f44bed1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ -# Getting Started with ghpool +# Getting Started with octobroker -ghpool is a GitHub API proxy for teams running **AI coding agents**: it pools +octobroker is a GitHub API proxy for teams running **AI coding agents**: it pools GitHub credentials server-side, caches read traffic, and (via MCP) gives agents GitHub's official tool surface **without any GitHub credential in the agent container**. This guide takes you from zero to a working setup in @@ -21,13 +21,13 @@ repos you care about. ```sh # grab a binary from releases, or: cargo build --release -export GHPOOL_PORT=8080 -export GHPOOL_ALLOWED_OWNERS=your-org -export GHPOOL_PAT_MAIN=ghp_xxxx # any GHPOOL_PAT_ env var joins the pool -export GHPOOL_MCP_ENABLED=true -./ghpool -# INFO ghpool: MCP reverse proxy enabled → https://api.githubcopilot.com/mcp/readonly -# INFO ghpool: ghpool listening on 0.0.0.0:8080 +export OCTOBROKER_PORT=8080 +export OCTOBROKER_ALLOWED_OWNERS=your-org +export OCTOBROKER_PAT_MAIN=ghp_xxxx # any OCTOBROKER_PAT_ env var joins the pool +export OCTOBROKER_MCP_ENABLED=true +./octobroker +# INFO octobroker: MCP reverse proxy enabled → https://api.githubcopilot.com/mcp/readonly +# INFO octobroker: octobroker listening on 0.0.0.0:8080 ``` Smoke-test both surfaces: @@ -40,7 +40,7 @@ curl -s localhost:8080/stats | jq . ``` For anything beyond env-var basics, use a config file. Search order: -`GHPOOL_CONFIG` env var → `./config.toml` → `~/.config/ghpool/config.toml`. +`OCTOBROKER_CONFIG` env var → `./config.toml` → `~/.config/octobroker/config.toml`. Start from [config.example.toml](../config.example.toml). ```toml @@ -58,7 +58,7 @@ enabled = true ## Stage 1: connect an MCP agent -Point any Streamable-HTTP MCP client at ghpool. The agent needs **no GitHub +Point any Streamable-HTTP MCP client at octobroker. The agent needs **no GitHub token, no gh CLI, no git credentials** — this is the whole point. Kiro CLI (`~/.kiro/settings/mcp.json`) and most JSON-configured clients: @@ -73,7 +73,7 @@ Claude Code: claude mcp add --transport http github http://localhost:8080/mcp ``` -Ask the agent to list issues in one of your repos. Watch ghpool's log: +Ask the agent to list issues in one of your repos. Watch octobroker's log: ``` MCP initialize [via main] @@ -88,18 +88,18 @@ Notes for this stage: differ from the write server (`issue_read`, not `get_issue`) — agents discover them via `tools/list`. - **Anyone who can reach `/mcp` gets this read access** (network-trust - model). Keep ghpool cluster-internal; go to Stage 2 before widening. + model). Keep octobroker cluster-internal; go to Stage 2 before widening. ## Stage 2: per-agent authentication Add `[[mcp.agents]]` entries. The moment one exists, every `/mcp` request -must present a valid `X-Ghpool-Key`, and each agent is confined to an exact +must present a valid `X-Octobroker-Key`, and each agent is confined to an exact tool allowlist (default-deny — new upstream tools are denied until listed): ```toml [[mcp.agents]] id = "my-bot" -key = "env:GHPOOL_KEY_MYBOT" # 256-bit random; openssl rand -hex 32 +key = "env:OCTOBROKER_KEY_MYBOT" # 256-bit random; openssl rand -hex 32 tools = ["issue_read", "list_issues", "get_file_contents"] repos = ["your-org/your-repo"] # optional; omit = no repo restriction ``` @@ -109,17 +109,17 @@ most MCP clients expand `${ENV}`): ```json { "mcpServers": { "github": { - "url": "http://ghpool:8080/mcp", - "headers": { "X-Ghpool-Key": "${GHPOOL_KEY}" } } } } + "url": "http://octobroker:8080/mcp", + "headers": { "X-Octobroker-Key": "${OCTOBROKER_KEY}" } } } } ``` -The key is a **ghpool credential, not a GitHub credential**: a leak is -bounded by the agent's allowlist and revoked by editing ghpool config. +The key is a **octobroker credential, not a GitHub credential**: a leak is +bounded by the agent's allowlist and revoked by editing octobroker config. Rotate with zero downtime via `keys = ["env:OLD", "env:NEW"]`. Sessions are bound to the agent that opened them — another agent presenting the same session ID gets 403. -Terminate TLS in front of ghpool in production — the key travels in a header. +Terminate TLS in front of octobroker in production — the key travels in a header. ## Stage 3: GitHub App backend @@ -137,11 +137,11 @@ installation. ```toml [mcp.github_app] app_id = "123456" -private_key = "aws:secretsmanager:ghpool/app:private_key" +private_key = "aws:secretsmanager:octobroker/app:private_key" owner = "your-org" # installation auto-discovered (or set installation_id) ``` -ghpool now mints and refreshes tokens itself (`minted GitHub App +octobroker now mints and refreshes tokens itself (`minted GitHub App installation token … expires in 3599s` in the log). Sessions never outlive the token they started with — at expiry the client transparently re-initializes. The PAT pool keeps serving REST/GraphQL. @@ -158,11 +158,11 @@ enable_writes = true # max_inflight_writes = 4 # per-agent concurrency cap [mcp.audit] -path = "/var/lib/ghpool/mcp-audit.jsonl" +path = "/var/lib/octobroker/mcp-audit.jsonl" [[mcp.agents]] id = "my-bot" -key = "env:GHPOOL_KEY_MYBOT" +key = "env:OCTOBROKER_KEY_MYBOT" tools = ["issue_read", "create_issue", "add_issue_comment"] repos = ["your-org/your-repo"] # exact entries → GitHub-enforced token scoping ``` @@ -181,7 +181,7 @@ What you get: Read the audit log: ```sh -jq -c 'select(.phase=="result")' /var/lib/ghpool/mcp-audit.jsonl | tail +jq -c 'select(.phase=="result")' /var/lib/octobroker/mcp-audit.jsonl | tail ``` ## Deploying on Kubernetes / ECS @@ -193,52 +193,52 @@ deploy just forces clients to re-initialize). Minimal K8s shape: # Secret for the pool credential (or use IRSA + aws:secretsmanager: refs) apiVersion: v1 kind: Secret -metadata: { name: ghpool-secrets, namespace: ghpool } +metadata: { name: octobroker-secrets, namespace: octobroker } stringData: { pat: ghp_xxx, agent-key: } --- apiVersion: apps/v1 kind: Deployment -metadata: { name: ghpool, namespace: ghpool } +metadata: { name: octobroker, namespace: octobroker } spec: replicas: 1 - selector: { matchLabels: { app: ghpool } } + selector: { matchLabels: { app: octobroker } } template: - metadata: { labels: { app: ghpool } } + metadata: { labels: { app: octobroker } } spec: containers: - - name: ghpool - image: ghcr.io/openabdev/ghpool:latest # or your build + - name: octobroker + image: ghcr.io/openabdev/octobroker:latest # or your build env: - - { name: GHPOOL_CONFIG, value: /etc/ghpool/config.toml } + - { name: OCTOBROKER_CONFIG, value: /etc/octobroker/config.toml } - name: GITHUB_TOKEN - valueFrom: { secretKeyRef: { name: ghpool-secrets, key: pat } } - - name: GHPOOL_KEY_MYBOT - valueFrom: { secretKeyRef: { name: ghpool-secrets, key: agent-key } } - volumeMounts: [{ name: config, mountPath: /etc/ghpool }] + valueFrom: { secretKeyRef: { name: octobroker-secrets, key: pat } } + - name: OCTOBROKER_KEY_MYBOT + valueFrom: { secretKeyRef: { name: octobroker-secrets, key: agent-key } } + volumeMounts: [{ name: config, mountPath: /etc/octobroker }] readinessProbe: { httpGet: { path: /healthz, port: 8080 } } volumes: - name: config - configMap: { name: ghpool-config } + configMap: { name: octobroker-config } --- apiVersion: v1 kind: Service -metadata: { name: ghpool, namespace: ghpool } +metadata: { name: octobroker, namespace: octobroker } spec: type: ClusterIP # cluster-internal only - selector: { app: ghpool } + selector: { app: octobroker } ports: [{ port: 8080, targetPort: 8080 }] ``` -Agents in the cluster use `http://ghpool.ghpool.svc.cluster.local:8080/mcp`. +Agents in the cluster use `http://octobroker.octobroker.svc.cluster.local:8080/mcp`. On ECS, run it as a Service Connect service and use -`http://ghpool.:8080/mcp`. Egress required: +`http://octobroker.:8080/mcp`. Egress required: `api.github.com` + `api.githubcopilot.com`. ## Troubleshooting | Symptom | Meaning | Fix | |---------|---------|-----| -| `401 X-Ghpool-Key header required` | Agents are configured; request has no/invalid key | Add the header to the client config; check the secret value | +| `401 X-Octobroker-Key header required` | Agents are configured; request has no/invalid key | Add the header to the client config; check the secret value | | `403 tool not permitted by agent policy` | Tool not on the agent's `tools` allowlist | Add it (deliberate: new tools are denied by default) | | `403 write tools are not enabled` | Write-classified tool without `enable_writes` | Complete Stage 4 | | `403 call has no resolvable repository target` | Agent has `repos` but the call's arguments name no repo (e.g. `search_code`) | Expected: repo-restricted agents can't use repo-less tools; remove `repos` or use repo-scoped tools | diff --git a/ghp/Cargo.lock b/obk/Cargo.lock similarity index 99% rename from ghp/Cargo.lock rename to obk/Cargo.lock index 4f1d64b..bff5daa 100644 --- a/ghp/Cargo.lock +++ b/obk/Cargo.lock @@ -157,7 +157,7 @@ dependencies = [ ] [[package]] -name = "ghp" +name = "obk" version = "0.1.0" dependencies = [ "reqwest", diff --git a/ghp/Cargo.toml b/obk/Cargo.toml similarity index 68% rename from ghp/Cargo.toml rename to obk/Cargo.toml index 0d4a2e4..d4061f4 100644 --- a/ghp/Cargo.toml +++ b/obk/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "ghp" +name = "obk" version = "0.1.1" edition = "2021" -description = "gh CLI shim that routes read commands through ghpool" +description = "gh CLI shim that routes read commands through octobroker" [dependencies] reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking"] } diff --git a/ghp/src/main.rs b/obk/src/main.rs similarity index 95% rename from ghp/src/main.rs rename to obk/src/main.rs index fab51e7..bb52d0f 100644 --- a/ghp/src/main.rs +++ b/obk/src/main.rs @@ -3,36 +3,36 @@ use std::process::{Command, exit}; fn main() { let args: Vec = env::args().skip(1).collect(); - let ghpool_url = env::var("GHPOOL_URL") - .unwrap_or_else(|_| "http://ghpool.openab.local:8080".to_string()); + let octobroker_url = env::var("OCTOBROKER_URL") + .unwrap_or_else(|_| "http://octobroker.openab.local:8080".to_string()); // Handle version if args.first().map(|s| s.as_str()) == Some("version") || args.first().map(|s| s.as_str()) == Some("--version") { - println!("ghp version {}", env!("CARGO_PKG_VERSION")); + println!("obk version {}", env!("CARGO_PKG_VERSION")); let gh = find_real_gh(); let _ = Command::new(&gh).arg("--version").status(); exit(0); } - // git credential helper protocol: `ghp git-credential ` + // git credential helper protocol: `obk git-credential ` // Configure as the ONLY helper for github.com (the empty --replace-all // entry clears inherited helpers so nothing broader can be consulted): // git config --global --replace-all credential."https://github.com".helper "" - // git config --global --add credential."https://github.com".helper "!ghp git-credential" + // git config --global --add credential."https://github.com".helper "!obk git-credential" // git config --global credential."https://github.com".useHttpPath true if args.first().map(|s| s.as_str()) == Some("git-credential") { - exit(git_credential(args.get(1).map(|s| s.as_str()), &ghpool_url)); + exit(git_credential(args.get(1).map(|s| s.as_str()), &octobroker_url)); } - // Try to handle as a pooled read via ghpool REST - if let Some(code) = try_pooled(&args, &ghpool_url) { + // Try to handle as a pooled read via octobroker REST + if let Some(code) = try_pooled(&args, &octobroker_url) { exit(code); } // Writes / unsupported commands: fall through to real gh let gh = find_real_gh(); let status = Command::new(&gh).args(&args).status().unwrap_or_else(|e| { - eprintln!("ghp: failed to exec {}: {}", gh, e); + eprintln!("obk: failed to exec {}: {}", gh, e); exit(1); }); exit(status.code().unwrap_or(1)); @@ -219,8 +219,8 @@ fn http_get(url: &str) -> Option { reqwest::blocking::get(url).ok()?.text().ok() } -/// Git credential helper backed by ghpool's /git-credential endpoint: -/// exchanges GHPOOL_KEY for a short-lived, single-repo GitHub App +/// Git credential helper backed by octobroker's /git-credential endpoint: +/// exchanges OCTOBROKER_KEY for a short-lived, single-repo GitHub App /// installation token. Only the `get` operation does anything; `store` /// and `erase` are no-ops (tokens are ephemeral, nothing to persist). /// @@ -231,7 +231,7 @@ fn git_credential(op: Option<&str>, base: &str) -> i32 { Some("get") => {} Some("store") | Some("erase") => return 0, _ => { - eprintln!("usage: ghp git-credential "); + eprintln!("usage: obk git-credential "); return 1; } } @@ -240,7 +240,7 @@ fn git_credential(op: Option<&str>, base: &str) -> i32 { if std::io::stdin().read_to_string(&mut input).is_err() { return 1; } - match credential_plan(&input, env::var("GHPOOL_KEY").ok()) { + match credential_plan(&input, env::var("OCTOBROKER_KEY").ok()) { CredentialPlan::Decline => 1, CredentialPlan::Quit => credential_quit(), CredentialPlan::Fetch { repo, key } => match fetch_git_credential(base, &repo, &key) { @@ -265,11 +265,11 @@ enum CredentialPlan { Decline, /// Recognized github.com request that cannot be served. The caller emits /// `quit=true` so git stops the helper cascade instead of falling - /// through to osxkeychain/GCM/gh auth — that would bypass ghpool repo + /// through to osxkeychain/GCM/gh auth — that would bypass octobroker repo /// policy and audit. Quit, - /// Recognized and serviceable: exchange GHPOOL_KEY for a short-lived - /// single-repo token via ghpool. + /// Recognized and serviceable: exchange OCTOBROKER_KEY for a short-lived + /// single-repo token via octobroker. Fetch { repo: String, key: String }, } @@ -314,7 +314,7 @@ fn fetch_git_credential(base: &str, repo: &str, key: &str) -> Option<(String, St let client = reqwest::blocking::Client::new(); let resp = client .get(&url) - .header("X-Ghpool-Key", key) + .header("X-Octobroker-Key", key) .timeout(std::time::Duration::from_secs(15)) .send() .ok()?; @@ -352,7 +352,7 @@ fn parse_credential_input(input: &str) -> std::collections::HashMap Option { let mut parts = path.trim_start_matches('/').splitn(3, '/'); let owner = parts.next().filter(|s| !s.is_empty())?; @@ -402,7 +402,7 @@ fn jq_extract(val: &serde_json::Value, expr: &str) -> String { } fn find_real_gh() -> String { - // Look for gh-real first (when ghp replaces /usr/bin/gh) + // Look for gh-real first (when obk replaces /usr/bin/gh) for dir in env::var("PATH").unwrap_or_default().split(':') { let candidate = format!("{}/gh-real", dir); if std::path::Path::new(&candidate).exists() { @@ -557,7 +557,7 @@ mod git_credential_tests { #[test] fn test_plan_declines_unrecognized_hosts() { // Non-GitHub hosts decline quietly (exit 1) so other helpers can - // serve them — ghp claims no authority there. + // serve them — obk claims no authority there. for input in [ "protocol=https\nhost=gitlab.com\npath=o/r.git\n", "protocol=https\nhost=gist.github.com\npath=abc123.git\n", @@ -676,7 +676,7 @@ mod git_credential_tests { #[test] fn test_fetch_fails_closed_on_server_errors() { - // 403 from ghpool (policy denial) → None → caller emits quit=true + // 403 from octobroker (policy denial) → None → caller emits quit=true let base = mock_server(http_response("403 Forbidden", "")); assert_eq!(fetch_git_credential(&base, "o/r", "k"), None); diff --git a/ghp/tests/git_credential_cli.rs b/obk/tests/git_credential_cli.rs similarity index 79% rename from ghp/tests/git_credential_cli.rs rename to obk/tests/git_credential_cli.rs index af52a1d..09b7cd7 100644 --- a/ghp/tests/git_credential_cli.rs +++ b/obk/tests/git_credential_cli.rs @@ -1,4 +1,4 @@ -//! End-to-end tests of the `ghp git-credential` helper contract against the +//! End-to-end tests of the `obk git-credential` helper contract against the //! real binary: a recognized github.com request must emit `quit=true` (exit //! 0) on every failure — never fall through to another credential helper — //! while non-GitHub hosts decline quietly (exit 1, no output). @@ -6,28 +6,28 @@ use std::io::Write; use std::process::{Command, Stdio}; -/// Run `ghp git-credential get` with the given stdin and optional -/// GHPOOL_KEY. GHPOOL_URL points at a closed port so any fetch attempt +/// Run `obk git-credential get` with the given stdin and optional +/// OCTOBROKER_KEY. OCTOBROKER_URL points at a closed port so any fetch attempt /// fails without touching the network. fn run_get(input: &str, key: Option<&str>) -> (String, i32) { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_ghp")); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_obk")); cmd.args(["git-credential", "get"]) - .env_remove("GHPOOL_KEY") - .env("GHPOOL_URL", "http://127.0.0.1:1") + .env_remove("OCTOBROKER_KEY") + .env("OCTOBROKER_URL", "http://127.0.0.1:1") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()); if let Some(k) = key { - cmd.env("GHPOOL_KEY", k); + cmd.env("OCTOBROKER_KEY", k); } - let mut child = cmd.spawn().expect("spawn ghp"); + let mut child = cmd.spawn().expect("spawn obk"); child .stdin .as_mut() .unwrap() .write_all(input.as_bytes()) .unwrap(); - let out = child.wait_with_output().expect("wait ghp"); + let out = child.wait_with_output().expect("wait obk"); ( String::from_utf8_lossy(&out.stdout).into_owned(), out.status.code().expect("exit code"), @@ -73,10 +73,10 @@ fn non_github_host_declines_quietly() { #[test] fn store_and_erase_are_noops() { for op in ["store", "erase"] { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_ghp")); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_obk")); let out = cmd .args(["git-credential", op]) - .env_remove("GHPOOL_KEY") + .env_remove("OCTOBROKER_KEY") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) diff --git a/scripts/e2e-mcp.sh b/scripts/e2e-mcp.sh index a88a350..4d9f229 100755 --- a/scripts/e2e-mcp.sh +++ b/scripts/e2e-mcp.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# E2E test: ghpool MCP reverse proxy against GitHub's hosted MCP server. +# E2E test: octobroker MCP reverse proxy against GitHub's hosted MCP server. # # Requires: # GITHUB_TOKEN — a user token (PAT or `gh auth token`); the Actions built-in # installation token is NOT accepted by the hosted MCP server. -# GHPOOL_BIN — path to the ghpool binary (default: ./target/debug/ghpool) +# OCTOBROKER_BIN — path to the octobroker binary (default: ./target/debug/octobroker) # jq, python3 # # Usage: GITHUB_TOKEN=$(gh auth token) ./scripts/e2e-mcp.sh @@ -13,10 +13,10 @@ # exits non-zero at the end if any failed. Hard setup errors abort explicitly. set -uo pipefail -BIN="${GHPOOL_BIN:-./target/debug/ghpool}" +BIN="${OCTOBROKER_BIN:-./target/debug/octobroker}" WORKDIR="$(mktemp -d)" -LOG="${WORKDIR}/ghpool.log" -# --max-time must exceed ghpool's upstream POST timeout (120s) so a slow but +LOG="${WORKDIR}/octobroker.log" +# --max-time must exceed octobroker's upstream POST timeout (120s) so a slow but # healthy upstream call is not cut off client-side. CURL=(curl -s --connect-timeout 5 --max-time 130) @@ -33,10 +33,10 @@ check() { } cleanup() { - [ -n "${GHPOOL_PID:-}" ] && kill "${GHPOOL_PID}" 2>/dev/null + [ -n "${OCTOBROKER_PID:-}" ] && kill "${OCTOBROKER_PID}" 2>/dev/null # Preserve the server logs for CI artifact upload on failure - [ "${fail:-1}" -gt 0 ] && [ -f "${LOG}" ] && cp "${LOG}" ./ghpool-e2e.log 2>/dev/null - [ "${fail:-1}" -gt 0 ] && [ -f "${WORKDIR}/ghpool-app.log" ] && cp "${WORKDIR}/ghpool-app.log" ./ghpool-e2e-app.log 2>/dev/null + [ "${fail:-1}" -gt 0 ] && [ -f "${LOG}" ] && cp "${LOG}" ./octobroker-e2e.log 2>/dev/null + [ "${fail:-1}" -gt 0 ] && [ -f "${WORKDIR}/octobroker-app.log" ] && cp "${WORKDIR}/octobroker-app.log" ./octobroker-e2e-app.log 2>/dev/null rm -rf "${WORKDIR}" } trap cleanup EXIT @@ -48,7 +48,7 @@ fi # Pick a free port to avoid CI collisions PORT="${PORT:-$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')}" -# Bind and connect over IPv4 explicitly: ghpool binds 0.0.0.0, but "localhost" +# Bind and connect over IPv4 explicitly: octobroker binds 0.0.0.0, but "localhost" # can resolve to ::1 on dual-stack hosts and fail to connect. BASE="http://127.0.0.1:${PORT}" @@ -62,15 +62,15 @@ token = "env:GITHUB_TOKEN" enabled = true EOF -echo "starting ghpool (${BIN}) on :${PORT}" -GHPOOL_CONFIG="${WORKDIR}/config.toml" "${BIN}" > "${LOG}" 2>&1 & -GHPOOL_PID=$! +echo "starting octobroker (${BIN}) on :${PORT}" +OCTOBROKER_CONFIG="${WORKDIR}/config.toml" "${BIN}" > "${LOG}" 2>&1 & +OCTOBROKER_PID=$! for _ in $(seq 1 20); do "${CURL[@]}" -f "${BASE}/healthz" > /dev/null 2>&1 && break sleep 0.5 done -"${CURL[@]}" -f "${BASE}/healthz" > /dev/null || { echo "ghpool failed to start"; cat "${LOG}"; exit 1; } +"${CURL[@]}" -f "${BASE}/healthz" > /dev/null || { echo "octobroker failed to start"; cat "${LOG}"; exit 1; } JSON_H=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream") @@ -92,7 +92,7 @@ jq_ok() { jq -e "$1" "$2" > /dev/null 2>&1; } echo "1. initialize (no client Authorization header)" "${CURL[@]}" -D "${WORKDIR}/init-headers.txt" -o "${WORKDIR}/init-body.txt" \ -X POST "${BASE}/mcp" "${JSON_H[@]}" \ - -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ghpool-e2e","version":"0"}}}' + -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"octobroker-e2e","version":"0"}}}' check "initialize returns 200" grep -q "^HTTP/1.1 200" "${WORKDIR}/init-headers.txt" SID="$(grep -i "^mcp-session-id:" "${WORKDIR}/init-headers.txt" | tr -d '\r' | awk '{print $2}')" check "Mcp-Session-Id returned" test -n "${SID}" @@ -120,9 +120,9 @@ check "read tool present (issue_read)" \ check "no write tools listed — readonly enforced" \ jq_ok '[.result.tools[].name | select(test("^(create_|update_|delete_|add_)"))] | length == 0' "${WORKDIR}/tools.json" -echo "4. tools/call issue_read on openabdev/ghpool#15" +echo "4. tools/call issue_read on openabdev/octobroker#15" "${CURL[@]}" -X POST "${BASE}/mcp" "${JSON_H[@]}" "${SESS_H[@]}" \ - -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"issue_read","arguments":{"method":"get","owner":"openabdev","repo":"ghpool","issue_number":15}}}' \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"issue_read","arguments":{"method":"get","owner":"openabdev","repo":"octobroker","issue_number":15}}}' \ -o "${WORKDIR}/issue-raw.txt" sse_json "${WORKDIR}/issue-raw.txt" > "${WORKDIR}/issue.json" check "issue_read returned issue #15" \ @@ -130,7 +130,7 @@ check "issue_read returned issue #15" \ echo "5. negative: write tool call is rejected" "${CURL[@]}" -X POST "${BASE}/mcp" "${JSON_H[@]}" "${SESS_H[@]}" \ - -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"ghpool","title":"e2e-must-not-exist"}}}' \ + -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"octobroker","title":"e2e-must-not-exist"}}}' \ -o "${WORKDIR}/write-raw.txt" sse_json "${WORKDIR}/write-raw.txt" > "${WORKDIR}/write.json" check "create_issue rejected (error or unknown tool)" \ @@ -156,10 +156,10 @@ check "tools/call audit-logged" grep -q "MCP tools/call issue_read" "${LOG}" # ── Optional: GitHub App credential backend mode (2b) ──────────────────── # Runs when App credentials are provided (CI passes the repo secrets). if [ -n "${APP_ID:-}" ] && [ -n "${APP_PRIVATE_KEY:-}" ]; then - echo "9. App-backend mode (ghpool mints installation tokens itself)" + echo "9. App-backend mode (octobroker mints installation tokens itself)" APP_PORT="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')" APP_BASE="http://127.0.0.1:${APP_PORT}" - APP_LOG="${WORKDIR}/ghpool-app.log" + APP_LOG="${WORKDIR}/octobroker-app.log" cat > "${WORKDIR}/config-app.toml" < "${APP_LOG}" 2>&1 & + OCTOBROKER_CONFIG="${WORKDIR}/config-app.toml" "${BIN}" > "${APP_LOG}" 2>&1 & APP_PID=$! trap '[ -n "${APP_PID:-}" ] && kill "${APP_PID}" 2>/dev/null; cleanup' EXIT @@ -178,11 +178,11 @@ EOF "${CURL[@]}" -f "${APP_BASE}/healthz" > /dev/null 2>&1 && break sleep 0.5 done - "${CURL[@]}" -f "${APP_BASE}/healthz" > /dev/null || { echo "app-mode ghpool failed to start"; cat "${APP_LOG}"; exit 1; } + "${CURL[@]}" -f "${APP_BASE}/healthz" > /dev/null || { echo "app-mode octobroker failed to start"; cat "${APP_LOG}"; exit 1; } "${CURL[@]}" -D "${WORKDIR}/app-init-h.txt" -o "${WORKDIR}/app-init-b.txt" \ -X POST "${APP_BASE}/mcp" "${JSON_H[@]}" \ - -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ghpool-e2e-app","version":"0"}}}' + -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"octobroker-e2e-app","version":"0"}}}' check "app-mode initialize returns 200" grep -q "^HTTP/1.1 200" "${WORKDIR}/app-init-h.txt" ASID="$(grep -i "^mcp-session-id:" "${WORKDIR}/app-init-h.txt" | tr -d '\r' | awk '{print $2}')" check "app-mode session established" test -n "${ASID}" @@ -190,7 +190,7 @@ EOF check "session pinned to App credential" grep -q "MCP session pinned to credential github-app" "${APP_LOG}" "${CURL[@]}" -X POST "${APP_BASE}/mcp" "${JSON_H[@]}" -H "Mcp-Session-Id: ${ASID}" -H "MCP-Protocol-Version: ${PROTO}" \ - -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"method":"get","owner":"openabdev","repo":"ghpool","issue_number":15}}}' \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"method":"get","owner":"openabdev","repo":"octobroker","issue_number":15}}}' \ -o "${WORKDIR}/app-issue-raw.txt" sse_json "${WORKDIR}/app-issue-raw.txt" > "${WORKDIR}/app-issue.json" check "app-mode issue_read works" \ diff --git a/src/app_token.rs b/src/app_token.rs index 1bf078b..6b64a6c 100644 --- a/src/app_token.rs +++ b/src/app_token.rs @@ -231,7 +231,7 @@ impl AppTokenProvider { .post(&url) .header("Authorization", format!("Bearer {}", jwt)) .header("Accept", "application/vnd.github+json") - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))); + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))); if !body.is_empty() { req = req.json(&body); } @@ -289,7 +289,7 @@ impl AppTokenProvider { .get(&url) .header("Authorization", format!("Bearer {}", jwt)) .header("Accept", "application/vnd.github+json") - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .send() .await .map_err(|e| format!("installation owner verification failed: {}", e))?; @@ -333,7 +333,7 @@ impl AppTokenProvider { .get(&path) .header("Authorization", format!("Bearer {}", jwt)) .header("Accept", "application/vnd.github+json") - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .send() .await .map_err(|e| format!("installation discovery failed: {}", e))?; @@ -541,14 +541,14 @@ pub(crate) mod tests { assert_eq!(MINTS.load(Ordering::SeqCst), 2); // Scoped envelopes are cached independently of installation-wide - let s1 = p.token_scoped(&["ghpool".into()]).await.unwrap(); + let s1 = p.token_scoped(&["octobroker".into()]).await.unwrap(); assert_eq!(s1.token, "ghs_mock_3"); // same envelope (different order) hits the cache - let s2 = p.token_scoped(&["ghpool".into()]).await.unwrap(); + let s2 = p.token_scoped(&["octobroker".into()]).await.unwrap(); assert_eq!(s2.token, "ghs_mock_3"); assert_eq!(MINTS.load(Ordering::SeqCst), 3); // different envelope mints separately - let s3 = p.token_scoped(&["ghpool".into(), "openab".into()]).await.unwrap(); + let s3 = p.token_scoped(&["octobroker".into(), "openab".into()]).await.unwrap(); assert_eq!(s3.token, "ghs_mock_4"); assert_eq!(MINTS.load(Ordering::SeqCst), 4); } diff --git a/src/audit.rs b/src/audit.rs index f4b9541..7786954 100644 --- a/src/audit.rs +++ b/src/audit.rs @@ -199,7 +199,7 @@ mod tests { fn tmp_path(name: &str) -> String { std::env::temp_dir() - .join(format!("ghpool-audit-{}-{}.jsonl", name, std::process::id())) + .join(format!("octobroker-audit-{}-{}.jsonl", name, std::process::id())) .to_str() .unwrap() .to_string() @@ -220,7 +220,7 @@ mod tests { fn test_records_roundtrip() { let path = tmp_path("roundtrip"); let sink = AuditSink::open(&path).unwrap(); - let repo = ("openabdev".to_string(), "ghpool".to_string()); + let repo = ("openabdev".to_string(), "octobroker".to_string()); sink.record_request(&call("create_issue", Some(&repo)), &["owner".into(), "title".into()]) .unwrap(); @@ -238,7 +238,7 @@ mod tests { assert_eq!(lines.len(), 2); assert_eq!(lines[0]["phase"], "request"); assert_eq!(lines[0]["tool"], "create_issue"); - assert_eq!(lines[0]["repo"], "openabdev/ghpool"); + assert_eq!(lines[0]["repo"], "openabdev/octobroker"); assert_eq!(lines[0]["arg_keys"], serde_json::json!(["owner", "title"])); assert_eq!(lines[0]["agent"], "bot-a"); assert_eq!(lines[1]["phase"], "result"); diff --git a/src/config.rs b/src/config.rs index d68a390..c494bc7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,7 +57,7 @@ impl Default for CacheConfig { } /// MCP reverse proxy configuration (Phase 1: read-only). -/// When enabled, ghpool proxies MCP Streamable HTTP traffic on /mcp to the +/// When enabled, octobroker proxies MCP Streamable HTTP traffic on /mcp to the /// GitHub-hosted MCP server, injecting a pooled credential upstream so agents /// never hold a GitHub token. #[derive(Clone, Deserialize)] @@ -71,7 +71,7 @@ pub struct McpConfig { #[serde(default)] pub enable_writes: bool, /// Enable the /git-credential endpoint: repository-scoped agents can - /// exchange their X-Ghpool-Key for a short-lived, single-repo GitHub App + /// exchange their X-Octobroker-Key for a short-lived, single-repo GitHub App /// installation token usable as a git-over-HTTPS credential /// (username `x-access-token`). Hard requirements mirror enable_writes: /// [[mcp.agents]], an App backend, and [mcp.audit] (every issuance is @@ -94,7 +94,7 @@ pub struct McpConfig { pub max_inflight_writes: usize, /// Per-agent authentication + default-deny tool allowlists (Phase 2a). /// Empty = Phase 1 network-trust mode (no agent authn on /mcp). - /// Non-empty = every /mcp request must present a valid X-Ghpool-Key. + /// Non-empty = every /mcp request must present a valid X-Octobroker-Key. #[serde(default)] pub agents: Vec, /// GitHub App credential backend (Phase 2b). When configured, the MCP @@ -166,7 +166,7 @@ pub struct GithubAppsEntry { #[derive(Clone, Deserialize)] pub struct McpAgentConfig { pub id: String, - /// Shared key presented via X-Ghpool-Key (single-key form). Supports the + /// Shared key presented via X-Octobroker-Key (single-key form). Supports the /// same secret reference formats as identity tokens; resolved at load. #[serde(default)] pub key: Option, @@ -387,7 +387,7 @@ impl Config { return config; } Err(e) => { - // Most likely a typo'd GHPOOL_CONFIG — don't fail silently + // Most likely a typo'd OCTOBROKER_CONFIG — don't fail silently tracing::warn!("cannot read config at {}: {} — falling back to env-only mode", path, e); } } @@ -396,13 +396,13 @@ impl Config { // Fallback: env vars only let identities = Self::identities_from_env(); - let allowed_owners = std::env::var("GHPOOL_ALLOWED_OWNERS") + let allowed_owners = std::env::var("OCTOBROKER_ALLOWED_OWNERS") .unwrap_or_default() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); - let port = std::env::var("GHPOOL_PORT") + let port = std::env::var("OCTOBROKER_PORT") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(default_port()); @@ -413,14 +413,14 @@ impl Config { } /// Config file search order: - /// 1. GHPOOL_CONFIG env var (explicit always wins; if set but unreadable, + /// 1. OCTOBROKER_CONFIG env var (explicit always wins; if set but unreadable, /// a warning is logged and no other file is tried) /// 2. ./config.toml (repo-local dev) - /// 3. $XDG_CONFIG_HOME/ghpool/config.toml (default ~/.config/ghpool/) + /// 3. $XDG_CONFIG_HOME/octobroker/config.toml (default ~/.config/octobroker/) /// /// Returns None when nothing is found → env-only mode. fn resolve_config_path() -> Option { - if let Ok(p) = std::env::var("GHPOOL_CONFIG") { + if let Ok(p) = std::env::var("OCTOBROKER_CONFIG") { return Some(p); } if std::path::Path::new("config.toml").exists() { @@ -430,7 +430,7 @@ impl Config { .ok() .filter(|s| !s.is_empty()) .or_else(|| std::env::var("HOME").ok().map(|h| format!("{}/.config", h)))?; - let xdg_path = format!("{}/ghpool/config.toml", xdg_base); + let xdg_path = format!("{}/octobroker/config.toml", xdg_base); if std::path::Path::new(&xdg_path).exists() { return Some(xdg_path); } @@ -478,22 +478,22 @@ impl Config { } fn apply_env_overrides(&mut self) { - if let Ok(v) = std::env::var("GHPOOL_PORT") { + if let Ok(v) = std::env::var("OCTOBROKER_PORT") { if let Ok(p) = v.parse() { self.port = p; } } - if let Ok(v) = std::env::var("GHPOOL_ALLOWED_OWNERS") { + if let Ok(v) = std::env::var("OCTOBROKER_ALLOWED_OWNERS") { self.allowed_owners = v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); } - if let Ok(v) = std::env::var("GHPOOL_MCP_ENABLED") { + if let Ok(v) = std::env::var("OCTOBROKER_MCP_ENABLED") { self.mcp.enabled = matches!(v.to_lowercase().as_str(), "1" | "true" | "yes"); } } fn identities_from_env() -> Vec { std::env::vars() - .filter(|(k, _)| k.starts_with("GHPOOL_PAT_")) + .filter(|(k, _)| k.starts_with("OCTOBROKER_PAT_")) .map(|(k, v)| IdentityConfig { - id: k.strip_prefix("GHPOOL_PAT_").unwrap().to_lowercase(), + id: k.strip_prefix("OCTOBROKER_PAT_").unwrap().to_lowercase(), token: v, }) .collect() @@ -564,17 +564,17 @@ mod tests { // Single test covering the whole chain to avoid parallel-test races // on process-global state (env vars + cwd; no other test touches // either). - let tmp = std::env::temp_dir().join(format!("ghpool-cfg-test-{}", std::process::id())); - let ghpool_dir = tmp.join("ghpool"); + let tmp = std::env::temp_dir().join(format!("octobroker-cfg-test-{}", std::process::id())); + let octobroker_dir = tmp.join("octobroker"); let cwd_dir = tmp.join("cwd"); - fs::create_dir_all(&ghpool_dir).unwrap(); + fs::create_dir_all(&octobroker_dir).unwrap(); fs::create_dir_all(&cwd_dir).unwrap(); // Run from an empty cwd so a developer's local ./config.toml doesn't // affect the outcome. let orig_cwd = std::env::current_dir().unwrap(); std::env::set_current_dir(&cwd_dir).unwrap(); - std::env::remove_var("GHPOOL_CONFIG"); + std::env::remove_var("OCTOBROKER_CONFIG"); std::env::set_var("XDG_CONFIG_HOME", &tmp); assert_eq!(Config::resolve_config_path(), None, "no file anywhere → env-only"); @@ -589,7 +589,7 @@ mod tests { fs::remove_file(cwd_dir.join("config.toml")).unwrap(); // XDG file exists → picked up - let xdg_file = ghpool_dir.join("config.toml"); + let xdg_file = octobroker_dir.join("config.toml"); fs::write(&xdg_file, "port = 1234\n").unwrap(); assert_eq!( Config::resolve_config_path().as_deref(), @@ -597,15 +597,15 @@ mod tests { "XDG path found" ); - // Explicit GHPOOL_CONFIG wins over XDG, even if the path doesn't exist - std::env::set_var("GHPOOL_CONFIG", "/nonexistent/override.toml"); + // Explicit OCTOBROKER_CONFIG wins over XDG, even if the path doesn't exist + std::env::set_var("OCTOBROKER_CONFIG", "/nonexistent/override.toml"); assert_eq!( Config::resolve_config_path().as_deref(), Some("/nonexistent/override.toml"), "explicit env var always wins" ); - std::env::remove_var("GHPOOL_CONFIG"); + std::env::remove_var("OCTOBROKER_CONFIG"); std::env::remove_var("XDG_CONFIG_HOME"); std::env::set_current_dir(orig_cwd).unwrap(); fs::remove_dir_all(&tmp).ok(); diff --git a/src/git_credential.rs b/src/git_credential.rs index 6f86188..48897aa 100644 --- a/src/git_credential.rs +++ b/src/git_credential.rs @@ -1,13 +1,13 @@ //! Git-over-HTTPS credential issuance (`/git-credential`). //! -//! Repository-scoped agents exchange their `X-Ghpool-Key` for a short-lived +//! Repository-scoped agents exchange their `X-Octobroker-Key` for a short-lived //! GitHub App installation token scoped to EXACTLY ONE repository, usable as //! a git HTTPS credential (`x-access-token:`). This closes the last //! long-lived-credential gap for agents: pushes authenticate as the App //! (`[bot]`), expire within the hour, and every issuance is fail-closed //! audited. //! -//! Request: GET /git-credential?repo=/ (X-Ghpool-Key header) +//! Request: GET /git-credential?repo=/ (X-Octobroker-Key header) //! Response: {"username":"x-access-token","password":"…","expires_at":…} //! //! Policy stack (all fail-closed): @@ -375,14 +375,14 @@ mod tests { .method("GET") .uri(format!("/git-credential?repo={}", repo)); if let Some(k) = key { - b = b.header("x-ghpool-key", k); + b = b.header("x-octobroker-key", k); } b.body(Body::empty()).unwrap() } fn audit_tmp(name: &str) -> String { std::env::temp_dir() - .join(format!("ghpool-gitcred-{}-{}.jsonl", name, std::process::id())) + .join(format!("octobroker-gitcred-{}-{}.jsonl", name, std::process::id())) .to_str() .unwrap() .to_string() diff --git a/src/main.rs b/src/main.rs index 742f285..49a2167 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ struct AppState { #[tokio::main] async fn main() { tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("ghpool=info".parse().unwrap())) + .with_env_filter(EnvFilter::from_default_env().add_directive("octobroker=info".parse().unwrap())) .with_timer(tracing_subscriber::fmt::time::LocalTime::new( time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]").unwrap(), )) @@ -129,7 +129,7 @@ async fn main() { let app = app.with_state(state); let addr = format!("0.0.0.0:{}", config.port); - tracing::info!("ghpool listening on {}", addr); + tracing::info!("octobroker listening on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); } @@ -196,7 +196,7 @@ async fn proxy( // Forward request let mut req = state.http.get(&url) .header("Authorization", format!("Bearer {}", identity.token)) - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .header("Accept", "application/vnd.github+json"); if let Some(version) = headers.get("x-github-api-version") { @@ -278,7 +278,7 @@ async fn proxy_raw( let result = state.cache.get_or_insert_raw(&cache_key, async move { let resp = state_for_fetch.http.get(&url) .header("Authorization", format!("Bearer {}", token)) - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .header("Accept", &accept) .send() .await @@ -372,7 +372,7 @@ async fn graphql_proxy( let resp = state.http.post("https://api.github.com/graphql") .header("Authorization", &auth_header) - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .header("Content-Type", "application/json") .body(body.to_vec()) .send() @@ -420,7 +420,7 @@ async fn resolve_token_user(state: &AppState, auth_header: &str) -> String { } let user = match state.http.get("https://api.github.com/user") .header("Authorization", auth_header) - .header("User-Agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .send() .await { @@ -520,7 +520,7 @@ mod tests { #[test] fn test_is_allowed_path() { let owners = vec!["openabdev".to_string(), "oablab".to_string()]; - assert!(is_allowed_path("/repos/openabdev/ghpool/pulls/1", &owners)); + assert!(is_allowed_path("/repos/openabdev/octobroker/pulls/1", &owners)); assert!(is_allowed_path("/repos/oablab/chi/issues", &owners)); assert!(!is_allowed_path("/repos/evil/repo/pulls/1", &owners)); // Non-repo paths are allowed diff --git a/src/mcp.rs b/src/mcp.rs index c8ba107..e376220 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -18,7 +18,7 @@ //! NOTE: `allowed_owners` is NOT enforced on /mcp in Phase 1 — doing so //! requires tool-argument inspection. Access is bounded by the pooled token's //! own permissions and the read-only upstream. Per-agent policy is tracked in -//! https://github.com/openabdev/ghpool/issues/17. +//! https://github.com/openabdev/octobroker/issues/17. use futures_util::StreamExt as _; @@ -52,9 +52,9 @@ const FWD_HEADERS: &[&str] = &[ "last-event-id", ]; -/// ghpool-owned MCP tools are namespaced so they cannot collide with tools +/// octobroker-owned MCP tools are namespaced so they cannot collide with tools /// exposed by GitHub's hosted MCP server. -const MINIMIZE_COMMENT_TOOL: &str = "ghpool_review_minimize_comment"; +const MINIMIZE_COMMENT_TOOL: &str = "octobroker_review_minimize_comment"; const GITHUB_GRAPHQL_URL: &str = "https://api.github.com/graphql"; const MINIMIZE_CLASSIFIERS: &[&str] = &[ "ABUSE", @@ -181,7 +181,7 @@ pub async fn mcp_proxy( } } - // Local ghpool-owned tools are writes even when the upstream policy block + // Local octobroker-owned tools are writes even when the upstream policy block // is bypassed (for example, in no-agent/network-trust mode). Keep the // local mutation path fail-closed and before credential resolution. if frame.as_ref().and_then(|f| f.tool.as_deref()) == Some(MINIMIZE_COMMENT_TOOL) @@ -332,7 +332,7 @@ pub async fn mcp_proxy( } } - // ghpool-owned review tools are handled locally. All policy and audit + // octobroker-owned review tools are handled locally. All policy and audit // checks above still apply; the upstream GitHub MCP server is not asked // to interpret a tool it does not expose. if frame.as_ref().and_then(|f| f.tool.as_deref()) == Some(MINIMIZE_COMMENT_TOOL) { @@ -487,7 +487,7 @@ pub async fn mcp_proxy( } } - // Add ghpool-owned tools only to an authenticated agent's advertised + // Add octobroker-owned tools only to an authenticated agent's advertised // surface when that agent explicitly allowlists the tool. The upstream // response remains untouched for all other agents and requests. if frame.as_ref().map(|f| f.method.as_str()) == Some("tools/list") @@ -774,7 +774,7 @@ async fn execute_graphql( .http .post(graphql_url) .bearer_auth(cred.token()) - .header("user-agent", concat!("ghpool/", env!("CARGO_PKG_VERSION"))) + .header("user-agent", concat!("octobroker/", env!("CARGO_PKG_VERSION"))) .header("content-type", "application/json") .json(payload) .timeout(std::time::Duration::from_secs(POST_TIMEOUT_SECS)) @@ -1623,7 +1623,7 @@ impl Drop for InFlightGuard { /// Phase 2a agent authentication. /// - No [[mcp.agents]] configured → Phase 1 network-trust mode: Ok(None). -/// - Agents configured → every request must present a valid X-Ghpool-Key; +/// - Agents configured → every request must present a valid X-Octobroker-Key; /// missing or unknown keys get 401 with a JSON-RPC error body. pub(crate) fn authenticate<'a>( state: &'a AppState, @@ -1633,17 +1633,17 @@ pub(crate) fn authenticate<'a>( if agents.is_empty() { return Ok(None); } - let Some(presented) = headers.get("x-ghpool-key").and_then(|v| v.to_str().ok()) else { - tracing::warn!("MCP request rejected: missing X-Ghpool-Key"); - return Err(Box::new(rpc_error(StatusCode::UNAUTHORIZED, "X-Ghpool-Key header required"))); + let Some(presented) = headers.get("x-octobroker-key").and_then(|v| v.to_str().ok()) else { + tracing::warn!("MCP request rejected: missing X-Octobroker-Key"); + return Err(Box::new(rpc_error(StatusCode::UNAUTHORIZED, "X-Octobroker-Key header required"))); }; for agent in agents { if agent.keys.iter().any(|k| keys_match(k, presented)) { return Ok(Some(agent)); } } - tracing::warn!("MCP request rejected: invalid X-Ghpool-Key"); - Err(Box::new(rpc_error(StatusCode::UNAUTHORIZED, "invalid X-Ghpool-Key"))) + tracing::warn!("MCP request rejected: invalid X-Octobroker-Key"); + Err(Box::new(rpc_error(StatusCode::UNAUTHORIZED, "invalid X-Octobroker-Key"))) } /// Compare keys via SHA-256 digests. Comparing fixed-length digests of both @@ -1699,7 +1699,7 @@ fn build_upstream_headers( h.insert("authorization", format!("Bearer {}", token).parse().ok()?); h.insert( "user-agent", - concat!("ghpool/", env!("CARGO_PKG_VERSION")).parse().expect("static ua header"), + concat!("octobroker/", env!("CARGO_PKG_VERSION")).parse().expect("static ua header"), ); for name in FWD_HEADERS { if let Some(v) = client.get(*name) { @@ -1942,7 +1942,7 @@ data: "id":1,"result":{"tools":[]}} #[test] fn test_parse_frame_tools_call() { - let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_issue","arguments":{"owner":"openabdev","repo":"ghpool"}}}"#; + let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_issue","arguments":{"owner":"openabdev","repo":"octobroker"}}}"#; let f = parse_frame(body).unwrap(); assert_eq!(f.method, "tools/call"); assert_eq!(f.tool.as_deref(), Some("get_issue")); @@ -2055,7 +2055,7 @@ data: "id":1,"result":{"tools":[]}} auth: Option, toolsets: Option, tools_hdr: Option, - ghpool_key: Option, + octobroker_key: Option, session: Option, body: String, } @@ -2081,7 +2081,7 @@ data: "id":1,"result":{"tools":[]}} auth: get("authorization"), toolsets: get("x-mcp-toolsets"), tools_hdr: get("x-mcp-tools"), - ghpool_key: get("x-ghpool-key"), + octobroker_key: get("x-octobroker-key"), session: get("mcp-session-id"), body: body_str.clone(), }); @@ -2368,7 +2368,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#, - &[("x-ghpool-key", "wrong-key")], + &[("x-octobroker-key", "wrong-key")], )) .await .unwrap(); @@ -2386,7 +2386,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2398,8 +2398,8 @@ data: "id":1,"result":{"tools":[]}} assert_eq!(reqs[0].tools_hdr.as_deref(), Some("issue_read,get_file_contents")); // agent mode: global toolsets NOT injected assert!(reqs[0].toolsets.is_none()); - // the ghpool key itself never goes upstream - assert!(reqs[0].ghpool_key.is_none()); + // the octobroker key itself never goes upstream + assert!(reqs[0].octobroker_key.is_none()); // pooled token injected as usual assert_eq!(reqs[0].auth.as_deref(), Some("Bearer token-alice")); } @@ -2414,7 +2414,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_file_contents","arguments":{}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2434,7 +2434,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2453,7 +2453,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":0,"method":"initialize"}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2461,7 +2461,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#, - &[("x-ghpool-key", "key-a"), ("mcp-session-id", "mock-sess-1")], + &[("x-octobroker-key", "key-a"), ("mcp-session-id", "mock-sess-1")], )) .await .unwrap(); @@ -2483,7 +2483,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_issues","arguments":{}}}"#, - &[("x-ghpool-key", "key-b")], + &[("x-octobroker-key", "key-b")], )) .await .unwrap(); @@ -2496,7 +2496,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_issues","arguments":{}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2520,7 +2520,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":0,"method":"initialize"}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2534,7 +2534,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{}}}"#, - &[("x-ghpool-key", "key-b"), ("mcp-session-id", "mock-sess-1")], + &[("x-octobroker-key", "key-b"), ("mcp-session-id", "mock-sess-1")], )) .await .unwrap(); @@ -2546,7 +2546,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"issue_read","arguments":{}}}"#, - &[("x-ghpool-key", "key-a"), ("mcp-session-id", "mock-sess-1")], + &[("x-octobroker-key", "key-a"), ("mcp-session-id", "mock-sess-1")], )) .await .unwrap(); @@ -2567,7 +2567,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#, - &[("x-ghpool-key", "key-a"), ("mcp-session-id", "old-sess")], + &[("x-octobroker-key", "key-a"), ("mcp-session-id", "old-sess")], )) .await .unwrap(); @@ -2585,7 +2585,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#, - &[("x-ghpool-key", key)], + &[("x-octobroker-key", key)], )) .await .unwrap(); @@ -2597,7 +2597,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"not_allowed","arguments":{}}}"#, - &[("x-ghpool-key", "new-key")], + &[("x-octobroker-key", "new-key")], )) .await .unwrap(); @@ -2628,8 +2628,8 @@ data: "id":1,"result":{"tools":[]}} ); let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"ghpool","title":"x"}}}"#, - &[("x-ghpool-key", "key-a")], + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"octobroker","title":"x"}}}"#, + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2645,12 +2645,12 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream().await; let state = test_state_full( &["alice"], &url, &[], - vec![agent_with_repos("bot-a", "key-a", &["issue_read"], &["openabdev/ghpool"])], + vec![agent_with_repos("bot-a", "key-a", &["issue_read"], &["openabdev/octobroker"])], ); let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"ghpool","issue_number":15}}}"#, - &[("x-ghpool-key", "key-a")], + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"octobroker","issue_number":15}}}"#, + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2663,12 +2663,12 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream().await; let state = test_state_full( &["alice"], &url, &[], - vec![agent_with_repos("bot-a", "key-a", &["issue_read"], &["openabdev/ghpool"])], + vec![agent_with_repos("bot-a", "key-a", &["issue_read"], &["openabdev/octobroker"])], ); let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"openab","issue_number":1}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2687,7 +2687,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_code","arguments":{"query":"foo"}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2708,7 +2708,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"anything","issue_number":1}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2727,7 +2727,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_code","arguments":{"query":"foo"}}}"#, - &[("x-ghpool-key", "key-a")], + &[("x-octobroker-key", "key-a")], )) .await .unwrap(); @@ -2738,7 +2738,7 @@ data: "id":1,"result":{"tools":[]}} #[tokio::test] async fn test_phase1_mode_unaffected_by_write_classification() { // No agents → Phase 1 network-trust mode: the readonly upstream is - // the write barrier; ghpool does not block (backward compatible). + // the write barrier; octobroker does not block (backward compatible). let (url, captured) = spawn_mock_upstream().await; let state = test_state_full(&["alice"], &url, &[], vec![]); let resp = mcp_app(state) @@ -2752,7 +2752,7 @@ data: "id":1,"result":{"tools":[]}} assert_eq!(captured.lock().unwrap().len(), 1); } - // ---- ghpool-owned tool: handler-level tests (mock GraphQL) ---- + // ---- octobroker-owned tool: handler-level tests (mock GraphQL) ---- /// Mock api.github.com/graphql: VerifyComment queries answer with the /// given author/repo (viewer is always the App bot identity, with the @@ -2785,7 +2785,7 @@ data: "id":1,"result":{"tools":[]}} } Json(serde_json::json!({ "data": { - "viewer": {"login": "oab-ghpool[bot]"}, + "viewer": {"login": "oab-octobroker[bot]"}, "node": { "author": {"login": author_login}, "issue": {"repository": {"nameWithOwner": node_repo}} @@ -2824,12 +2824,12 @@ data: "id":1,"result":{"tools":[]}} #[tokio::test] async fn test_minimize_accepts_app_bot_authored_comment() { - // The live API returns viewer "oab-ghpool[bot]" but Bot comment - // authors carry the bare "oab-ghpool" login — the ownership check + // The live API returns viewer "oab-octobroker[bot]" but Bot comment + // authors carry the bare "oab-octobroker" login — the ownership check // must accept the App's own comments (empirically verified shape). - let (gql, mutations) = spawn_mock_graphql("oab-ghpool", "openabdev/ghpool", false).await; + let (gql, mutations) = spawn_mock_graphql("oab-octobroker", "openabdev/octobroker", false).await; let state = test_state_full(&["alice"], "http://unused", &[], vec![]); - let frame = minimize_frame("openabdev", "ghpool", "OUTDATED"); + let frame = minimize_frame("openabdev", "octobroker", "OUTDATED"); let out = handle_minimize_comment(&state, &app_cred(), &frame, &gql).await; assert_eq!(out.http_status, 200); assert_eq!(out.tool_error, Some(false)); @@ -2841,9 +2841,9 @@ data: "id":1,"result":{"tools":[]}} #[tokio::test] async fn test_minimize_rejects_human_authored_comment() { - let (gql, mutations) = spawn_mock_graphql("chaodu-agent", "openabdev/ghpool", false).await; + let (gql, mutations) = spawn_mock_graphql("chaodu-agent", "openabdev/octobroker", false).await; let state = test_state_full(&["alice"], "http://unused", &[], vec![]); - let frame = minimize_frame("openabdev", "ghpool", "OUTDATED"); + let frame = minimize_frame("openabdev", "octobroker", "OUTDATED"); let out = handle_minimize_comment(&state, &app_cred(), &frame, &gql).await; assert_eq!(out.tool_error, Some(true)); assert!(mutations.lock().unwrap().is_empty(), "no mutation for foreign authors"); @@ -2853,9 +2853,9 @@ data: "id":1,"result":{"tools":[]}} async fn test_minimize_rejects_repo_mismatch() { // node_id belongs to another repository than the policy-checked // owner/repo arguments — must be refused before the mutation. - let (gql, mutations) = spawn_mock_graphql("oab-ghpool", "openabdev/other-repo", false).await; + let (gql, mutations) = spawn_mock_graphql("oab-octobroker", "openabdev/other-repo", false).await; let state = test_state_full(&["alice"], "http://unused", &[], vec![]); - let frame = minimize_frame("openabdev", "ghpool", "OUTDATED"); + let frame = minimize_frame("openabdev", "octobroker", "OUTDATED"); let out = handle_minimize_comment(&state, &app_cred(), &frame, &gql).await; assert_eq!(out.http_status, StatusCode::FORBIDDEN.as_u16()); assert_eq!(out.tool_error, Some(true)); @@ -2865,15 +2865,15 @@ data: "id":1,"result":{"tools":[]}} #[tokio::test] async fn test_minimize_rejects_graphql_errors_and_bad_classifier() { // GraphQL soft errors (HTTP 200 + errors[]) fail closed. - let (gql, mutations) = spawn_mock_graphql("oab-ghpool", "openabdev/ghpool", true).await; + let (gql, mutations) = spawn_mock_graphql("oab-octobroker", "openabdev/octobroker", true).await; let state = test_state_full(&["alice"], "http://unused", &[], vec![]); - let frame = minimize_frame("openabdev", "ghpool", "OUTDATED"); + let frame = minimize_frame("openabdev", "octobroker", "OUTDATED"); let out = handle_minimize_comment(&state, &app_cred(), &frame, &gql).await; assert_eq!(out.tool_error, Some(true)); assert!(mutations.lock().unwrap().is_empty()); // Unknown classifier is rejected before any network call. - let frame = minimize_frame("openabdev", "ghpool", "WRONG"); + let frame = minimize_frame("openabdev", "octobroker", "WRONG"); let out = handle_minimize_comment(&state, &app_cred(), &frame, "http://127.0.0.1:1/").await; assert_eq!(out.tool_error, Some(true)); } @@ -2886,7 +2886,7 @@ data: "id":1,"result":{"tools":[]}} let state = test_state_full(&["alice"], &url, &[], vec![]); let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ghpool_review_minimize_comment","arguments":{"owner":"openabdev","repo":"ghpool","node_id":"MDU6SXNzdWUx","classifier":"OUTDATED"}}}"#, + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"octobroker_review_minimize_comment","arguments":{"owner":"openabdev","repo":"octobroker","node_id":"MDU6SXNzdWUx","classifier":"OUTDATED"}}}"#, &[], )) .await @@ -3056,7 +3056,7 @@ data: "id":1,"result":{"tools":[]}} fn audit_tmp(name: &str) -> String { std::env::temp_dir() - .join(format!("ghpool-mcp-audit-{}-{}.jsonl", name, std::process::id())) + .join(format!("octobroker-mcp-audit-{}-{}.jsonl", name, std::process::id())) .to_str().unwrap().to_string() } @@ -3113,7 +3113,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"ghpool","title":"secret title"}}}"#, + r#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"octobroker","title":"secret title"}}}"#, &[], )) .await @@ -3127,7 +3127,7 @@ data: "id":1,"result":{"tools":[]}} assert_eq!(records.len(), 2); assert_eq!(records[0]["phase"], "request"); assert_eq!(records[0]["tool"], "create_issue"); - assert_eq!(records[0]["repo"], "openabdev/ghpool"); + assert_eq!(records[0]["repo"], "openabdev/octobroker"); assert_eq!(records[0]["rpc_id"], 7); // argument VALUES never recorded assert!(!records[0].to_string().contains("secret title")); @@ -3233,13 +3233,13 @@ data: "id":1,"result":{"tools":[]}} #[test] fn test_scope_envelope() { // exact entries, single owner → repo names - let a = agent_with_repos("a", "k", &[], &["openabdev/ghpool", "openabdev/openab"]); - assert_eq!(scope_envelope(Some(&a)), vec!["ghpool", "openab"]); + let a = agent_with_repos("a", "k", &[], &["openabdev/octobroker", "openabdev/openab"]); + assert_eq!(scope_envelope(Some(&a)), vec!["octobroker", "openab"]); // wildcard → installation-wide let a = agent_with_repos("a", "k", &[], &["openabdev/*"]); assert!(scope_envelope(Some(&a)).is_empty()); // mixed owners → installation-wide - let a = agent_with_repos("a", "k", &[], &["openabdev/ghpool", "oablab/chi"]); + let a = agent_with_repos("a", "k", &[], &["openabdev/octobroker", "oablab/chi"]); assert!(scope_envelope(Some(&a)).is_empty()); // no restriction → installation-wide let a = agent("a", "k", &[]); @@ -3294,14 +3294,14 @@ data: "id":1,"result":{"tools":[]}} let sink = crate::audit::AuditSink::open(&path).unwrap(); let state = test_state_writes_enabled( &url, sink, - vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/ghpool"])], + vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/octobroker"])], 4, ); let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"ghpool","title":"t"}}}"#, - &[("x-ghpool-key", "key-w")], + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"octobroker","title":"t"}}}"#, + &[("x-octobroker-key", "key-w")], )) .await .unwrap(); @@ -3326,14 +3326,14 @@ data: "id":1,"result":{"tools":[]}} let sink = crate::audit::AuditSink::open(&path).unwrap(); let state = test_state_writes_enabled( &url, sink, - vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/ghpool"])], + vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/octobroker"])], 4, ); // wrong repo → 403 even with writes enabled let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"evil","repo":"other","title":"t"}}}"#, - &[("x-ghpool-key", "key-w")], + &[("x-octobroker-key", "key-w")], )) .await .unwrap(); @@ -3341,8 +3341,8 @@ data: "id":1,"result":{"tools":[]}} // tool off allowlist → 403 let resp = mcp_app(state) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_file","arguments":{"owner":"openabdev","repo":"ghpool"}}}"#, - &[("x-ghpool-key", "key-w")], + r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_file","arguments":{"owner":"openabdev","repo":"octobroker"}}}"#, + &[("x-octobroker-key", "key-w")], )) .await .unwrap(); @@ -3380,15 +3380,15 @@ data: "id":1,"result":{"tools":[]}} let sink = crate::audit::AuditSink::open(&path).unwrap(); let state = test_state_writes_enabled( &url, sink, - vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/ghpool"])], + vec![agent_with_repos("bot-w", "key-w", &["create_issue"], &["openabdev/octobroker"])], 1, ); // Saturate the cap by holding a guard, then issue a write let _held = InFlightGuard::try_acquire(&state.write_inflight, "bot-w", 1).unwrap(); let resp = mcp_app(state.clone()) .oneshot(post_frame( - r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"ghpool","title":"t"}}}"#, - &[("x-ghpool-key", "key-w")], + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"octobroker","title":"t"}}}"#, + &[("x-octobroker-key", "key-w")], )) .await .unwrap(); @@ -3442,7 +3442,7 @@ data: "id":1,"result":{"tools":[]}} auth: auth.clone(), toolsets: get("x-mcp-toolsets"), tools_hdr: get("x-mcp-tools"), - ghpool_key: get("x-ghpool-key"), + octobroker_key: get("x-octobroker-key"), session: session.clone(), body: body_str.clone(), }); @@ -3591,10 +3591,10 @@ data: "id":1,"result":{"tools":[]}} fn test_route_owners_and_owner_envelope() { let a = agent_with_repos( "a", "k", &[], - &["openabdev/openab", "OABLAB/chi", "openabdev/ghpool"], + &["openabdev/openab", "OABLAB/chi", "openabdev/octobroker"], ); assert_eq!(route_owners(&a), vec!["oablab", "openabdev"]); - assert_eq!(scope_envelope_for_owner(&a, "openabdev"), vec!["openab", "ghpool"]); + assert_eq!(scope_envelope_for_owner(&a, "openabdev"), vec!["openab", "octobroker"]); assert_eq!(scope_envelope_for_owner(&a, "oablab"), vec!["chi"]); assert!(scope_envelope_for_owner(&a, "other").is_empty()); // wildcard for the owner → installation-wide @@ -3610,7 +3610,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; let resp = mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -3655,7 +3655,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -3665,7 +3665,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"openab","issue_number":1}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3676,7 +3676,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"oablab","repo":"chi","issue_number":2}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3696,7 +3696,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -3704,7 +3704,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"evil","repo":"repo","issue_number":1}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3719,7 +3719,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"openab","issue_number":1}}}"#, - &[("x-ghpool-key", "key-b0")], + &[("x-octobroker-key", "key-b0")], )) .await .unwrap(); @@ -3768,7 +3768,7 @@ data: "id":1,"result":{"tools":[]}} // Call the LIVE route (oablab): routes are minted together, // so ANY expired route terminates the session as a whole. r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"oablab","repo":"chi","issue_number":1}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "dsid")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "dsid")], )) .await .unwrap(); @@ -3787,7 +3787,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"tag":"fail_secondary"}}"#, - &[("x-ghpool-key", "key-b0")], + &[("x-octobroker-key", "key-b0")], )) .await .unwrap(); @@ -3823,7 +3823,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -3831,7 +3831,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3864,7 +3864,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -3874,7 +3874,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state.clone()) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"openabdev","repo":"openab","issue_number":1}}}"#, - &[("x-ghpool-key", "key-intruder"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-intruder"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3885,7 +3885,7 @@ data: "id":1,"result":{"tools":[]}} let req = Request::builder() .method("DELETE") .uri("/mcp") - .header("x-ghpool-key", "key-intruder") + .header("x-octobroker-key", "key-intruder") .header("mcp-session-id", "sess-ghs_oablab") .body(Body::empty()) .unwrap(); @@ -3898,7 +3898,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"issue_read","arguments":{"owner":"oablab","repo":"chi","issue_number":2}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); @@ -3910,7 +3910,7 @@ data: "id":1,"result":{"tools":[]}} let (url, captured) = spawn_mock_upstream_multi().await; let state = test_state_multi(&url, false, None).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -3918,7 +3918,7 @@ data: "id":1,"result":{"tools":[]}} let req = Request::builder() .method("DELETE") .uri("/mcp") - .header("x-ghpool-key", "key-b0") + .header("x-octobroker-key", "key-b0") .header("mcp-session-id", "sess-ghs_oablab") .body(Body::empty()) .unwrap(); @@ -3952,7 +3952,7 @@ data: "id":1,"result":{"tools":[]}} // initialize as the repo-less agent: NO fan-out — single upstream // request, served by the pooled PAT, pinned normally let resp = mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b2pat")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b2pat")])) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -3971,7 +3971,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_code","arguments":{"query":"foo"}}}"#, - &[("x-ghpool-key", "key-b2pat"), ("mcp-session-id", "sess-token-alice")], + &[("x-octobroker-key", "key-b2pat"), ("mcp-session-id", "sess-token-alice")], )) .await .unwrap(); @@ -3989,7 +3989,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"openab","title":"t"}}}"#, - &[("x-ghpool-key", "key-b2pat")], + &[("x-octobroker-key", "key-b2pat")], )) .await .unwrap(); @@ -4009,7 +4009,7 @@ data: "id":1,"result":{"tools":[]}} let sink = crate::audit::AuditSink::open(&path).unwrap(); let state = test_state_multi(&url, true, Some(sink)).await; mcp_app(state.clone()) - .oneshot(post_frame(MULTI_INIT, &[("x-ghpool-key", "key-b0")])) + .oneshot(post_frame(MULTI_INIT, &[("x-octobroker-key", "key-b0")])) .await .unwrap(); captured.lock().unwrap().clear(); @@ -4017,7 +4017,7 @@ data: "id":1,"result":{"tools":[]}} let resp = mcp_app(state) .oneshot(post_frame( r#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"create_issue","arguments":{"owner":"openabdev","repo":"openab","title":"t"}}}"#, - &[("x-ghpool-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], + &[("x-octobroker-key", "key-b0"), ("mcp-session-id", "sess-ghs_oablab")], )) .await .unwrap(); diff --git a/src/policy.rs b/src/policy.rs index d030b97..638b1dc 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -118,10 +118,10 @@ mod tests { #[test] fn test_resolve_repo() { - let args = serde_json::json!({"owner": "openabdev", "repo": "ghpool", "issue_number": 15}); + let args = serde_json::json!({"owner": "openabdev", "repo": "octobroker", "issue_number": 15}); assert_eq!( resolve_repo(Some(&args)), - Some(("openabdev".to_string(), "ghpool".to_string())) + Some(("openabdev".to_string(), "octobroker".to_string())) ); // unresolvable shapes @@ -136,13 +136,13 @@ mod tests { #[test] fn test_repo_allowed_exact_and_wildcard() { - let allow = vec!["openabdev/ghpool".to_string(), "oablab/*".to_string()]; - assert!(repo_allowed(&allow, "openabdev", "ghpool")); - assert!(repo_allowed(&allow, "OpenABdev", "GHPool")); // case-insensitive + let allow = vec!["openabdev/octobroker".to_string(), "oablab/*".to_string()]; + assert!(repo_allowed(&allow, "openabdev", "octobroker")); + assert!(repo_allowed(&allow, "OpenABdev", "Octobroker")); // case-insensitive assert!(repo_allowed(&allow, "oablab", "chi")); assert!(repo_allowed(&allow, "oablab", "anything")); assert!(!repo_allowed(&allow, "openabdev", "openab")); - assert!(!repo_allowed(&allow, "evil", "ghpool")); + assert!(!repo_allowed(&allow, "evil", "octobroker")); } #[test]