4.0 KiB
4.0 KiB
Service registry
How operators register a downstream service so the broker can mint per-service tokens.
Location
~/.config/sherlock/services.d/<name>.toml — one file per service. The broker hot-reloads on change.
A built-in registry under internal/agent/profiles/… no, services are NOT built-in (unlike agent profiles). Every service is operator-registered.
Schema
[service]
name = "gitea" # unique; matches the file's basename
kind = "oidc-federated" # oidc-federated | own-oauth | static-pat
issuer = "https://id.alexandru.macocian.me/application/o/gitea/"
audience = "gitea.alexandru.macocian.me"
scopes = ["openid", "profile", "email", "read:repository"]
[exchange] # required when kind = "oidc-federated"
mode = "rfc8693" # rfc8693 | passthrough
subject_token_source = "authentik-id-token"
# Optional: override the token endpoint if it's not the issuer's default.
# token_endpoint = "https://id.alexandru.macocian.me/application/o/token/"
[mcp] # how the rendered MCP config refers to this service
env_var = "GITEA_TOKEN" # env var the MCP binary reads on startup
command = "gitea-mcp" # executable name (looked up on PATH)
args = []
kind = "own-oauth" extras
[oauth]
authorization_endpoint = "https://github.com/login/oauth/authorize"
token_endpoint = "https://github.com/login/oauth/access_token"
client_id = "Iv1.abc123"
# client_secret is read from a sibling `<name>.secret.age` file, never inline.
redirect_path = "/cb/github" # appended to the broker's loopback base URL
scopes = ["read:org", "read:user"]
kind = "static-pat" extras
[pat]
# Path to an age-encrypted file containing the PAT. Decrypted by the broker
# on demand using the operator's age key.
file = "~/.config/sherlock/secrets/dockerhub.pat.age"
Field reference
| Field | Required | Notes |
|---|---|---|
service.name |
yes | Must equal the filename stem. The broker rejects mismatches. |
service.kind |
yes | See auth-model.md for the three grant kinds. |
service.issuer |
oidc-federated only |
OIDC issuer URL; used for metadata discovery. |
service.audience |
oidc-federated only |
Goes into the audience parameter of the token-exchange request. |
service.scopes |
yes | Default scopes. Override per-invocation with sherlock run --scope. |
exchange.mode |
oidc-federated only |
rfc8693 does a real exchange; passthrough hands the existing ID token unchanged (only safe when audiences match). |
mcp.env_var |
yes | The MCP binary reads this env var on startup. Convention: <SERVICE>_TOKEN. |
mcp.command / mcp.args |
yes | Used by the wrapper when it renders the per-session MCP config for the agent. |
Loading
- The broker scans
~/.config/sherlock/services.d/*.tomlon startup. - On filesystem change (fsnotify), it re-parses and atomically swaps its registry.
- Parse errors are logged but do not crash the broker — the offending service is just marked unavailable.
Worked examples
Gitea (Phase 2)
[service]
name = "gitea"
kind = "oidc-federated"
issuer = "https://id.alexandru.macocian.me/application/o/gitea/"
audience = "gitea.alexandru.macocian.me"
scopes = ["openid", "profile", "read:repository", "write:issue"]
[exchange]
mode = "rfc8693"
subject_token_source = "authentik-id-token"
[mcp]
env_var = "GITEA_TOKEN"
command = "gitea-mcp"
args = []
gssh (Phase 3)
[service]
name = "gssh"
kind = "oidc-federated"
issuer = "https://id.alexandru.macocian.me/application/o/gssh/"
audience = "gssh.alexandru.macocian.me"
scopes = ["openid", "profile", "email"]
[exchange]
mode = "passthrough"
subject_token_source = "authentik-id-token"
[mcp]
env_var = "GSSH_TOKEN"
command = "gssh-mcp"
args = []
See gssh-integration.md for why passthrough is OK here.