A service that provides scoped GitHub credentials to autonomous bots, with policy-based auto-approval and human-in-the-loop approval flows.
When running multiple autonomous bots (coding agents, CI helpers, etc.), each bot needs GitHub access to:
- Read/write code
- Comment on issues and PRs
- Push commits
- Create branches and PRs
Current approaches have drawbacks:
- Shared PAT: All bots have same identity, no auditability, overly broad access
- Per-bot PAT: Manual token management, no central revocation, still broad scopes
- Per-bot GitHub App: Complex setup, overkill for small teams
A centralized credential broker that:
- Holds a single GitHub App's private key
- Issues short-lived, scoped tokens to bots on demand
- Enforces policy (which bot can access what)
- Supports human approval for sensitive requests
- Provides audit trail of all credential grants
┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────┐
│ Bot (machina) │────▶│ Credential Broker │────▶│ GitHub App │
│ Bot (runless) │ │ │ │ (org-installed)│
│ Bot (slop) │ │ - Policy engine │ └─────────────────┘
└─────────────────┘ │ - Approval queue │
│ - Audit log │
▲ └──────────┬──────────┘
│ │
│ ▼
│ ┌─────────────────────┐
└───────────────│ Human (via chat/ │
approval │ CLI/notification) │
└─────────────────────┘
Each bot has a known identity, verified by one of:
- API key: Simple pre-shared secret per bot
- Tailnet IP: If running on Tailscale, trust the source hostname
- mTLS: Bot presents client certificate
| Type | Use Case | Lifetime |
|---|---|---|
| Installation token | API calls, HTTPS git | 1 hour (GitHub limit) |
| Refreshable session | Long-running work | Auto-refreshes |
| Deploy key | SSH git access | Configurable |
Since multiple bots share one GitHub App, commits appear as app-name[bot].
To distinguish which bot made a commit:
- Commit message prefix:
[machina] fix: the bug - Co-authored-by trailer:
Co-authored-by: machina <machina@example.com> - Commit body metadata
The broker can enforce attribution format as part of policy.
bots:
machina-bot:
identity:
type: tailnet
hostname: machina-bot
auto_approve:
# Repos this bot can access without asking
- repo: "myorg/repo-a"
permissions: [contents:write, issues:write, pull_requests:write]
- repo: "myorg/repo-b"
permissions: [contents:read]
requires_approval:
# Requests matching these need human approval
- permissions: [admin]
- repo: "myorg/sensitive-*"
deny:
# Always deny these
- repo: "myorg/infrastructure"
defaults:
# Unknown bots or unmatched requests
requires_approval: true
approval_timeout: 24h- Check
denyrules → reject if match - Check
auto_approverules → issue token if match - Check
requires_approvalrules → queue for approval if match - Fall back to
defaults
PENDING → APPROVED → ISSUED
↓
DENIED
↓
EXPIRED (timeout)
Chat (primary for small teams)
🔐 Credential Request #a3f2
Bot: slopcannon-bot
Repo: myorg/new-project
Permissions: contents:write, issues:write
Reason: "Implementing feature from issue #45"
Requested: 2 minutes ago
[Approve] [Approve + Remember] [Deny]
- Approve: One-time approval, issue token
- Approve + Remember: Add to bot's auto_approve policy
- Deny: Reject with optional reason
CLI (for bulk/scripting)
# List pending
botcreds pending
# Approve
botcreds approve <request-id>
botcreds approve <request-id> --persist # add to auto-approve
botcreds approve --bot=machina --all # approve all pending for bot
# Deny
botcreds deny <request-id> --reason="Not authorized for this repo"
# Review recent grants
botcreds log --since=24hWebhook (for automation)
- POST to callback URL on approval/denial
- Enables bot to wait asynchronously
When approval is needed:
- Primary: Chat message to configured owner(s)
- Optional: Email, Slack, webhook
Include context:
- Which bot
- What repo/permissions
- Why (reason from bot)
- Quick-action buttons
from botcreds import BrokerClient
client = BrokerClient(
broker_url="https://creds.internal",
bot_id="machina-bot",
api_key=os.environ["BROKER_API_KEY"]
)
# Request credentials
creds = client.get_credentials(
repo="myorg/target-repo",
permissions=["contents:write", "issues:write"],
reason="Fixing bug #123",
wait_for_approval=True, # Block until approved (with timeout)
ttl="1h"
)
# Use for git
os.environ["GIT_ASKPASS"] = creds.git_askpass_helper
subprocess.run(["git", "clone", creds.repo_url])
# Use for API
github = Github(creds.token)
repo = github.get_repo("myorg/target-repo")Broker returns ready-to-use git credentials:
{
"token": "ghs_xxxx",
"expires_at": "2026-02-17T15:00:00Z",
"git": {
"clone_url": "https://x-access-token:ghs_xxxx@github.com/myorg/repo.git",
"askpass_script": "#!/bin/sh\necho ghs_xxxx"
},
"attribution": {
"name": "myapp[bot]",
"email": "123+myapp[bot]@users.noreply.github.com",
"trailer": "Bot: machina-bot"
}
}For long-running operations:
# Option 1: Auto-refresh wrapper
with client.session(repo="myorg/repo", permissions=["contents:write"]) as session:
# Token auto-refreshes before expiry
do_long_running_work(session.token)
# Option 2: Manual refresh
creds = client.get_credentials(...)
# ... work ...
if creds.expires_soon():
creds = client.refresh(creds)Every action logged:
{
"timestamp": "2026-02-17T14:30:00Z",
"event": "credential_issued",
"bot": "machina-bot",
"repo": "myorg/anvil",
"permissions": ["contents:write", "issues:write"],
"approval": "auto",
"token_id": "tok_abc123",
"expires_at": "2026-02-17T15:30:00Z",
"client_ip": "100.64.1.5",
"reason": "Implementing feature #45"
}Events:
credential_requestedcredential_issued(with approval type: auto/manual)credential_deniedcredential_refreshedcredential_revokedapproval_requestedapproval_grantedapproval_deniedapproval_expired
- Requests per bot
- Auto-approve vs manual approval ratio
- Approval latency (time to human response)
- Token usage (API calls made with issued tokens, if trackable)
- Denial rate
- Bot requesting unusual repos
- Spike in credential requests
- High denial rate
- Credentials unused (issued but never used)
- Only grant permissions actually needed
- Prefer repo-specific over org-wide
- Short TTLs by default (1 hour)
# Revoke specific token
botcreds revoke <token-id>
# Revoke all tokens for a bot
botcreds revoke --bot=machina-bot
# Revoke all tokens for a repo
botcreds revoke --repo=myorg/compromisedRevocation invalidates at GitHub level (installation token revocation API).
If a bot is compromised:
- Revoke all its active tokens
- Remove from policy (deny all)
- Audit recent activity
- Rotate bot's broker API key
- Broker is high-value target (holds GitHub App private key)
- Run on isolated infrastructure
- mTLS or Tailscale-only access
- No public internet exposure
- Regular key rotation
- Broker runs as a single process
- SQLite for state (pending approvals, audit log)
- Config file for policy
- Tailscale for bot access
- Broker behind load balancer
- PostgreSQL for state
- Policy in git (GitOps)
- Multiple approval channels
- Temporary elevated access: "Give machina admin for 1 hour"
- Access reviews: Periodic review of what bots have access to
- Cost tracking: Map token usage to API rate limits
- Multi-provider: Extend to GitLab, Bitbucket
- Secrets beyond GitHub: AWS credentials, database access, etc.
- Approval UX: Chat buttons vs CLI vs web UI — which is primary?
- Policy storage: File vs database vs git repo?
- Bot registration: Self-service or admin-only?
- Token caching: Should broker cache tokens or always mint fresh?
- Failure mode: What happens if broker is down? Bots blocked or cached creds?
- GitHub App setup + JWT auth
- Installation token minting
- Simple file-based policy (auto-approve only)
- Bot authentication via API key
- Basic audit log
- Pending request queue
- Chat notification + approval buttons
- CLI for approval management
- "Approve + remember" flow
- Bot SDK/client library
- Credential refresh
- Revocation
- Metrics/dashboard
This is a living document. Update as the design evolves.