Files
sherlock/docs/service-registry.md
T
amacocian 801bbcec09
CI / build (push) Has been cancelled
Phase 0
2026-05-24 21:53:20 +02:00

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

  1. The broker scans ~/.config/sherlock/services.d/*.toml on startup.
  2. On filesystem change (fsnotify), it re-parses and atomically swaps its registry.
  3. 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.