Integrations
rupu plugs into your source host and your issue tracker so agents can read issues, work in repos, open pull requests, and comment back — across more than one platform at once.
Plug rupu into your stack
Integrations are how rupu reaches out beyond your laptop. Two connector families do the work, and both surface as typed tools in rupu’s tool catalog (scm.*, issues.*, github.*, gitlab.*), so an agent calls the same tool regardless of which platform a call resolves to:
- Source control (SCM) — the
RepoConnector. List and read repos, clone working trees, read files by ref, open pull requests / merge requests. Ships for GitHub and GitLab. - Issue trackers — the
IssueConnector. Fetch and list issues, comment, create, and transition state. Ships for GitHub Issues, GitLab Issues, Jira, and Linear.
The catalog tools are served in three places, with one implementation behind all three: to an agent in-process, through its tools: grant; to a workflow action: step; and to an outside MCP client on /mcp of rupu cp serve, beside the operator tools (see MCP & tools). Per-platform connectors absorb the vendor quirks — GitLab MR vs GitHub PR vocabulary, nested namespaces, rate-limit headers — so your agent files stay portable: an agent that reads scm.prs.diff and posts scm.prs.comment works against GitHub and GitLab unchanged.
rupu auth login path as your model providers, stored in ~/.rupu/auth.json (mode 0600). rupu auth status picks up SCM rows automatically, and you can hold more than one account per platform. The same credential store unlocks the catalog tools on /mcp: log in once and every surface is authenticated.Source control (SCM)
The RepoConnector handles everything repo-shaped. When a run targets a repo or a PR (not a bare issue), rupu clones the repo into a tempdir and runs the agent there; issue targets skip the clone and the agent’s read tools work without a checkout.
| Platform | API | What rupu can do |
|---|---|---|
| GitHub | REST | List / get repos & branches, create branches, read files by ref, list / get PRs, fetch diffs, comment on PRs, open PRs (incl. draft), clone_to local checkout, dispatch a GitHub Actions workflow_dispatch. GitHub Enterprise Server via a base_url on the account. |
| GitLab | REST | The same surface, with MR vocabulary translated to scm.prs.*: list / get repos & branches, create branches, read files, list / get MRs, fetch diffs, comment, open MRs, clone_to, trigger a CI pipeline against a ref. Nested groups and self-managed instances (base_url on the account) supported. |
Run targets — the optional positional argument of rupu run and rupu workflow run — pick a platform by prefix. PRs use #N on GitHub and !N on GitLab (GitLab’s MR convention):
# repo working tree / PR / MR / issue github:owner/repo # repo github:owner/repo#42 # PR 42 github:owner/repo/issues/123 # issue 123 gitlab:group/project # repo gitlab:group/sub/project!7 # MR 7 gitlab:group/project/issues/9 # issue 9
The rupu repos CLI
List remote repos and manage the local repo registry (which checkout on disk maps to which repo ref):
rupu repos list [--platform github|gitlab] # repos across configured platforms (default: all) rupu repos attach github:owner/repo [PATH] # map a local checkout (default: cwd) to a repo ref rupu repos prefer github:owner/repo [PATH] # mark a checkout as the preferred path rupu repos tracked # list locally tracked checkouts rupu repos forget github:owner/repo [--path PATH] # forget a repo, or one tracked path
clone_to currently uses the public host’s clone URLs, so it refuses an account whose base_url is off github.com / gitlab.com (GitHub Enterprise, self-managed GitLab) rather than send that account’s token elsewhere. With clone_protocol = "ssh" clones produce git@<host>:<owner>/<repo>.git, drop the token entirely, and shell out to the system git (so host aliases, IdentityFile, ProxyJump and agent forwarding apply; git must be on PATH).
Issue trackers
The IssueConnector family lets agents read and write tracker state. All four trackers work with the issues.* tools (list, get, comment, create, update state); GitHub Issues and GitLab Issues also drive the rupu issues CLI, and Jira and Linear add native trigger sources (polling and webhook — see Triggers). Reading an issue’s existing comment thread (issues.comments, oldest-first) is GitHub-only today; the other trackers answer “not supported”. Linear and Jira are not repo / PR backends: no scm.* repo, branch, PR/MR or clone support.
| Tracker | Auth | What rupu reads / writes |
|---|---|---|
| GitHub Issues | PAT or device-code SSO | List, fetch (title, body, state, labels, author), read the comment thread, comment, create, transition open/closed. Full issues.* tools + rupu issues. Plus issue / PR / push event triggers. |
| GitLab Issues | PAT or browser-callback PKCE SSO | The same read/write surface except the comment-thread read (#N for issues; MRs use !N). Plus issue / MR / push event triggers. |
| Jira | API key — store as <email>:<api_token> |
Project-scoped IssueConnector (the issues.* tools, minus the comment-thread read) + native trigger source. Polls jira:<site>/<project> (or jira:<project> with [scm.jira].base_url); normalizes jira.issue.opened / jira.issue.updated and workflow-state transitions. |
| Linear | API key | Team-scoped IssueConnector (the issues.* tools, minus the comment-thread read) + native trigger source. Polls linear:<team-id>; normalizes linear.issue.opened / linear.issue.updated and native workflow-state changes. |
GitHub Projects (v2) also drives the tracker workflow vocabulary: when an issue-backed project item’s Status field changes, rupu maps it into portable issue.* aliases (e.g. issue.entered_workflow_state.ready_for_review). Projects native state mapping is webhook-only today — there is no poll_sources form for Projects yet.
The rupu issues CLI
Auto-detects the target repo from the cwd’s git origin remote when --repo is omitted; a bare #42 or 42 resolves against that remote too:
rupu issues list --state open --label triage # list issues (--state open|closed|all, --limit, --account) rupu issues show github:owner/repo/issues/42 # full body + metadata rupu issues run <workflow> #42 # run a workflow against an issue
Run a workflow against an issue
rupu issues run is a convenience wrapper for the canonical form, which takes the issue ref as a positional run-target:
rupu workflow run <workflow> github:owner/repo/issues/42
Issue targets do not clone — the workflow runs in the current checkout and binds the issue’s metadata into step prompts and when: filters as {{ issue.* }}:
| Field | Value |
|---|---|
issue.number | Issue number |
issue.title | Title |
issue.body | Description / body |
issue.labels | Label list |
issue.author | Reporter |
issue.state | open / closed |
The SCM and issue tools
All 18 connector tools in rupu’s tool catalog. Each accepts an optional platform? (or tracker?) that falls back to [scm.default] / [issues.default] from config when omitted, and an optional account that names one configured account outright (see multiple accounts). Schemas are generated from the typed argument structs and exposed on every tools/list response and in GET /api/tools.
| Tool | Kind | Description |
|---|---|---|
scm.repos.list | Read | List authenticated user’s repos on a platform |
scm.repos.get | Read | Fetch a single repo (default branch, clone URLs, visibility) |
scm.branches.list | Read | List branches with sha + protected flag |
scm.branches.create | Write | Create a new branch from a SHA |
scm.files.read | Read | Read a file at an optional ref; returns path + content + encoding |
scm.prs.list | Read | List PRs/MRs with state + author + limit filters |
scm.prs.get | Read | Fetch a single PR/MR (title, body, head/base branches, author) |
scm.prs.diff | Read | Fetch the unified-diff patch + file/add/delete counters |
scm.prs.comment | Write | Post a top-level comment on a PR/MR |
scm.prs.create | Write | Open a PR/MR; supports draft=true |
issues.list | Read | List issues with state + labels + author filters |
issues.get | Read | Fetch a single issue (title, body, state, labels, author) |
issues.comments | Read | Read an issue’s comment thread, oldest-first (GitHub only) |
issues.comment | Write | Comment on an issue |
issues.create | Write | Open a new issue with title + body + labels |
issues.update_state | Write | Transition an issue to open or closed |
github.workflows_dispatch | Write | Trigger a GitHub Actions workflow_dispatch |
gitlab.pipeline_trigger | Write | Trigger a GitLab CI pipeline against a ref |
The findings tools (findings.report, findings.verify, findings.query, findings.tag) sit in the same catalog; they are covered on the Findings page.
In an agent
An agent gets a connector tool by naming it in its tools: frontmatter — an exact name, a namespace (scm.*, issues.*) or *. A call to a tool the agent didn’t list fails with tool not in agent's tools: list. The run’s permission mode decides each call from the tool’s effect: the read tools (scm.prs.get, issues.list, …) are allowed in every mode, while the write tools act outside this machine (external effect — scm.prs.create, scm.branches.create, issues.comment, issues.create, github.workflows_dispatch, …) and are prompted for in ask and denied in readonly. See Agents.
--- name: issue-reporter tools: [read_file, grep, issues.list, issues.get, issues.comment, issues.create, issues.update_state] ---
Narrowing a workflow step: actions:
A workflow step’s actions: narrows the connector portion of its agent’s grant — the scm.*, issues.*, github.* and gitlab.* tools — to the intersection of the agent’s tools and the list. Every other tool (bash, read_file, the findings and coverage tools, dispatch) is never touched by it; those are governed by the agent’s tools: and the run mode. Entries use the tools: grammar; an unknown entry, or one naming no connector tool, is a parse-time error.
# agent grants: [issues.list, issues.get, issues.comment, issues.create, issues.update_state] steps: - id: triage agent: issue-reporter actions: [issues.list, issues.get] # narrowed to read-only for THIS step - id: report agent: issue-reporter actions: [] # unrestricted: the full agent grant
- An empty or absent
actions:means unrestricted: the step runs with the agent’s full grant. - A step can only shrink the connector set. Naming a tool the agent doesn’t grant does not add it (the runner logs a warning). What was narrowed away is recorded in the step transcript’s
tool_grantevent. - The narrowing reaches every child the step’s agent starts with
dispatch, in its process or its own: a child can’t call a connector tool the step couldn’t. - A non-empty
actions:on a remote step (host:/distribute:) is rejected at parse time rather than silently ignored, and so is one on anaction:step, whose single tool call is already explicit.
An action: step calls one connector tool directly, with no agent: action: issues.comment and a with: map checked against the tool’s schema at parse time. See the authoring reference.
From an outside client: /mcp
rupu cp serve serves the same catalog on its /mcp endpoint (streamable HTTP), beside the operator tools that wrap every control-plane route — among them repos.list, repos.tracked, repos.attach, repos.prefer, repos.forget, scm_accounts.list, scm_rules.list, scm_rules.bind and auth.status. rupu adds no permission layer of its own there; what runs is decided by the MCP client’s harness, and --tools narrows what is served (rupu cp serve --no-web --tools 'scm.*,issues.*'). Authentication to GitHub and GitLab is shared with the CLI through the credential store.
rupu mcp serve is deprecated. It used to print a stdio server. It now prints a deprecation notice and runs rupu cp serve --no-web on 127.0.0.1:7878; --transport and --mode are ignored and --tools is passed on. stdio is gone: point clients at the HTTP URL (a client that only speaks stdio can bridge with mcp-remote). Connection recipes for Claude Code, Cursor and Claude Desktop are on the MCP & tools page.Configuration
# ~/.rupu/config.toml or <repo>/.rupu/config.toml [scm.default] platform = "github" owner = "section9labs" repo = "rupu" [issues.default] tracker = "github" project = "section9labs/rupu" [scm.github] base_url = "https://api.github.com" # overridable for GHES timeout_ms = 30000 max_concurrency = 8 clone_protocol = "https" # https | ssh [scm.gitlab] base_url = "https://gitlab.com/api/v4" # overridable for self-hosted timeout_ms = 30000 max_concurrency = 6 clone_protocol = "https"
| Key | Meaning |
|---|---|
base_url | API root — override for GHES / self-hosted GitLab. Clone paths still use the public host (see clone_to above). |
timeout_ms | Total per-request deadline for this platform’s HTTP calls. Default 30000; 0 is treated as unset. |
max_concurrency | Per-platform semaphore size. Defaults: github 8, gitlab 6. Drop it to 4 if GitHub’s secondary rate limit bites. |
clone_protocol | https (default; token embedded in the URL) or ssh. An unrecognized value logs a warning and falls back to https. |
oauth_client_id | GitLab only: the Application ID rupu auth login --mode sso logs this account in as. Required for a self-managed instance; read from the global config only. |
kind | github or gitlab: the vendor a named account talks to. Unset means the table name itself is the vendor ([scm.github], [scm.gitlab]). |
Requests are cached for 5 minutes and retried up to 5 attempts per platform. A 401 is Unauthorized and a 403 with a missing-scope header is MissingScope (neither recoverable); other 403s and 429 are RateLimited, 404 NotFound, 409/422 write conflicts Conflict and 5xx Transient (all retried); 400 and 422 validation errors are BadRequest, and a connection refusal or timeout is Network.
Multiple accounts per platform
A single GitHub (or GitLab) account needs none of this — [scm.github], or just a stored credential, resolves as before. To hold two accounts of one platform — a work and a personal identity, or github.com alongside a GitHub Enterprise host — name them. [scm.<name>] is account-keyed: the table name is the identity and kind selects the vendor client.
# declares the account (writes [scm.<name>] kind = ...) and stores its credential in one step rupu auth login --account gh-work --kind github --mode sso rupu auth login --account gh-personal --kind github --mode sso
[scm.gh-work] kind = "github" [scm.gh-personal] kind = "github" [scm.corp-ghe] kind = "github" base_url = "https://git.example.internal/api/v3" # GitHub Enterprise # which account serves which repo [[scm.rules]] owner = "example-org/*" account = "gh-work" [[scm.rules]] path = "~/Code/work/*" account = "gh-work"
Each rule sets exactly one of owner (matched against the repo owner: example-org/* or a bare example-org) or path (a glob on the caller’s working directory; ~ expands to $HOME); a rule with both or neither fails config validation. Account selection runs in a fixed precedence, highest first:
- Explicit —
--account <name>onrupu issues list|show, or theaccountargument on anscm.*/issues.*tool. - Owner rule — the first matching
ownerglob. This is the tier daemons rely on: a webhook or cron poll knows the repo owner but has no cwd. - Path rule — the first matching
pathglob. - Sole account — only one account of that platform is configured, so nothing is ambiguous.
- Error — never a silent guess.
A targeted call that matches no rule with two or more candidates fails with the configured accounts and a ready-made fix line; a rule that fires but names an account with no live credential fails too, instead of falling through to a different identity. (A rule naming a live account of a different platform is simply skipped.) An unconfigured --account name errors immediately, listing what is configured. rupu repos list and scm.repos.list, which have no repo to key on, fan out across every account of the platform and tag each row with its account.
rupu scm accounts # accounts, kinds, base_urls, and the rules pointing at each rupu scm bind --owner 'example-org/*' --account gh-work # append an owner rule rupu scm bind --path '~/Code/oss/*' --account gh-personal rupu issues list --repo github:other/thing --account gh-work # one-off override
rupu run <target>, rupu workflow run <target> and rupu issues run have no --account flag; for an ambiguous repo, add a rule with rupu scm bind (which writes [[scm.rules]] through the same atomic config writer rupu auth login uses, so comments and formatting survive).How it fits together
The integrations close a loop. A new or labeled issue (or a tracker state change) wakes a workflow or an autoflow; rupu’s agents do the work in a cloned checkout; the SCM connector opens a PR / MR with the result; and the issue connector posts a comment back (a workflow’s notifyIssue) so the conversation stays where your team already lives.
Cross-references: Autoflows drive the long-running issue→PR loop with branch ownership; Triggers cover the polling and webhook tiers that wake workflows on tracker and SCM events.
Auth for integrations
Each platform stores its credential in ~/.rupu/auth.json via one command. These are SCM / tracker credentials — distinct from model-provider auth (for that, see Providers & Auth). --provider is an alias for --account.
rupu auth login --provider <github|gitlab|linear|jira> --mode <api-key|sso> rupu auth login --account <name> --kind <github|gitlab> --mode <api-key|sso> # a named account rupu auth status # SCM + tracker rows appear automatically
- GitHub — classic PAT (
read:user,repo,workflow,gist,read:org) or a device-code SSO flow. - GitLab — PAT (
api,read_user,read_repository,write_repository) or browser-callback PKCE SSO, which requestsopenid profile read_user write_repository api. See GitLab SSO below. - Linear — API-key mode only (used for polling / webhook normalization).
- Jira — API-key mode only; store the credential as
<email>:<api_token>in one secret for Jira Cloud polling.
GitLab SSO and token refresh
On gitlab.com, rupu auth login --provider gitlab --mode sso signs in as the GitLab CLI’s (glab’s) public OAuth application, the way GitHub SSO uses gh’s. A self-managed instance needs its own OAuth application: set its Application ID as oauth_client_id on the account in the global ~/.rupu/config.toml (auth login reads nothing else), and the authorize and token endpoints are derived from base_url (<instance>/oauth/authorize, <instance>/oauth/token). The login records the application and token endpoint on the credential, and its refreshes go there.
[scm.gl-corp] kind = "gitlab" base_url = "https://gitlab.example.internal/api/v4" oauth_client_id = "<application id>"
GitLab OAuth access tokens expire two hours after issue. Every GitLab connector refreshes its token through the credential store before a request when it is within five minutes of expiry — so a long-lived rupu cp serve (its /mcp included) or session keeps working — and the rotated token is persisted under auth.json.lock for every other process. A request GitLab refuses with a 401 is retried once with the token now in the store, so a fresh rupu auth login reaches processes that are already running. GitHub SSO tokens don’t expire.
rupu webhook serve are read from environment variables only — never config files, never the credential store: RUPU_GITHUB_WEBHOOK_SECRET, RUPU_GITLAB_WEBHOOK_TOKEN (GitLab’s shared token, not a signing secret), RUPU_LINEAR_WEBHOOK_SECRET and RUPU_JIRA_WEBHOOK_SECRET. Set only the vendors you receive from. See Triggers.