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
+
+
+
-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]