MuseHub Threat Model
Last updated: 2026-04-07
Author: gabriel
System Overview
MuseHub is the remote server for the Muse version control system. It stores pushed commits, hosts issue tracking and merge proposals, and serves as the hub for multi-agent orchestration via agentception. Two types of principals interact with MuseHub:
- Human users — authenticated via Ed25519 keypairs registered at account creation.
- Agent identities — short-lived Ed25519 keypairs registered programmatically by agentception at runtime, carrying an explicit capability scope and an expiry timestamp.
All communication is over HTTPS. There are no passwords, sessions, or bearer tokens. Every authenticated request carries a per-request Ed25519 signature.
Authentication: MSign
Protocol
Every authenticated request carries:
Authorization: MSign handle="gabriel" ts=1712345678 sig="<base64url>"
The signature covers the canonical message:
METHOD\n
PATH_WITH_QUERY\n
UNIX_TIMESTAMP\n
SHA256_HEX(body_bytes)
The server verifies the signature against the registered public key(s) for the handle. Replay protection: requests are rejected when |server_time - ts| > 30 seconds.
Key properties
| Property | Value |
|---|---|
| Algorithm | Ed25519 (RFC 8032) |
| Key storage (server) | Public key only, stored in musehub_auth_keys |
| Key storage (client) | Private key in ~/.muse/keys/*.pem |
| Replay window | 30 seconds |
| Clock skew tolerance | ±30 seconds |
Threat mitigations
| Threat | Mitigation |
|---|---|
| Replay attack | Timestamp in canonical message; server rejects if skew > 30s |
| Man-in-the-middle | HTTPS required; signature covers body hash, not just path |
| Key compromise (human) | muse auth keygen + revoke_identity; soft-delete immediately visible |
| Key compromise (agent) | Short TTL (2h by default); agentception revokes at run end |
| Forged handle | Handle is looked up in DB; must match a row with a registered key |
| Credential stuffing | No passwords; Ed25519 keys cannot be brute-forced |
| Timing oracle | Signature verification iterates all keys; failure path is constant |
Identity Hierarchy
gabriel (human, permanent)
└── agentception-service (service identity, 24h TTL)
└── agentception-{run_id} (ephemeral agent identity, 2h TTL)
Identity types
| Type | identity_type |
scope |
expires_at |
|---|---|---|---|
| Human | "human" |
null (unrestricted) |
null (never expires) |
| Service | "agent" |
["label:write", "issue:write", "proposal:write", "label:read", "issue:read"] |
24h from registration |
| Ephemeral agent | "agent" |
Subset of service scope | 2h from dispatch |
Identity lifecycle
- Registration:
POST /api/v1/identities/agents— requires a valid MSign token from the parent identity. Stores public key and scope. - Request signing: Agent signs requests with its private key. Server verifies against stored public key.
- Scope check:
require_scope("X")dependency rejects requests whereidentity.scopeis non-null and does not contain"X". - Expiry check:
_verify_msignrejects requests for identities whereexpires_at <= now. Checked before signature verification. - Revocation:
DELETE /api/v1/identities/{handle}— setsdeleted_at, immediately visible to all subsequent requests.
Capability Scopes
Scope enforcement is implemented in require_scope() (musehub/auth/request_signing.py). Rules:
scope = null— human identity; no capability restrictions. All authenticated operations are permitted.scope = [...]— agent identity; only listed tokens are permitted. Missing token → HTTP 403.- Empty scope
[]— agent is blocked from every scoped operation.
Scope taxonomy
| Token | Endpoints |
|---|---|
repo:write |
POST repos; DELETE repo; transfer ownership; PATCH settings; POST/stop sessions; rebuild symbol index |
issue:read |
GET issues, GET comments (authenticated private repos) |
issue:write |
POST/PATCH issues; POST/DELETE comments; set/remove milestone; reopen/close |
label:read |
GET label assignments (authenticated private repos) |
label:write |
POST/DELETE/PATCH labels; POST/DELETE label assignments on issues and proposals |
proposal:read |
GET proposals, GET reviews (authenticated private repos) |
proposal:write |
POST proposals; merge; POST comments; request/remove reviewers; submit reviews |
release:write |
POST releases; POST/DELETE release assets |
milestone:write |
POST/PATCH/DELETE milestones |
Who holds which scopes
| Principal | Scopes |
|---|---|
| gabriel (human) | All (unrestricted, scope=null) |
| agentception-service | issue:write, issue:read, label:write, label:read, proposal:write |
| agentception-{run_id} | Subset of parent scope, decided at dispatch |
Human identities carry scope = null and pass all scope checks unconditionally — there
are no human-only capabilities. Any operation can be granted to an agent by including the
relevant token in its scope list at registration time.
Defence-in-Depth Layers
Layer 1: Bot throttle (ASGI middleware)
BotThrottleMiddleware runs before any route handler. It short-circuits at the ASGI level for known-bad User-Agent patterns (scrapers, scanners). Exempt paths: /healthz, /static/, /mcp, /api/health/.
MSign-authenticated requests bypass the UA check entirely — cryptographic identity supersedes User-Agent heuristics.
Layer 2: Request signing (MSign)
Every route that mutates state requires a valid MSign token. Anonymous access is permitted only for read operations on public repos.
Layer 3: Scope enforcement
Write routes use Depends(require_scope("X")) instead of Depends(require_valid_token). This ensures agent identities are limited to their registered capability set.
Layer 4: Expiry enforcement
_verify_msign checks expires_at before attempting signature verification. An expired identity is rejected even if its private key is still cryptographically valid.
Layer 5: Soft-delete / revocation
deleted_at is checked in every identity lookup. Revoked identities are immediately invisible without requiring key rotation or cache invalidation.
Layer 6: Per-route slowapi rate limits
Routes are rate-limited by IP + identity. Authentication failures do not count against the route limit but are tracked by failure_limiter to detect credential stuffing.
Known Limitations and Future Work
| Item | Status |
|---|---|
| Scope enforcement on wire protocol (push/pull) | Not implemented. Wire operations use human keys; agents don't push. Tracked as future work. |
| Scope audit log | Not implemented. Scope denials are logged at WARNING level but not stored in DB. |
| Agent-to-agent delegation | Not implemented. An agent cannot grant sub-scope to another agent directly. |
| Key rotation without downtime | Supported — multiple keys per identity; verification tries all. |
| Scope changes without re-registration | Not supported. Scope is set at registration and cannot be modified. Revoke and re-register. |
Incident Response
Compromised human key
muse auth keygen --hub <hub>— generate a new key.DELETE /api/v1/identities/{handle}/keys/{key_id}— revoke the old key.- All subsequent requests using the old key are rejected immediately.
Compromised agent key
DELETE /api/v1/identities/{handle}— soft-delete the agent identity. Takes effect immediately.- Generate a new ephemeral identity for the next run.
Suspected replay attack
- Check access logs for duplicate
(handle, ts)pairs within the 30-second window. - If systematic, reduce
REPLAY_WINDOW_SECONDSand rotate the affected identity's key.