wyrd/tools
esc

    ↑↓ Move ↵ Open menagerie results first Search by Pagefind
    Releases

    Configuration

    Both services read one YAML file, /etc/menagerie/config.yml. Unknown keys, a second document and an empty file are errors, and the container does not start. The schema is the complete reference.

    workspace-root: /srv/menagerie/workspaces
    workspace-repository: ssh://git@github.com/example-org/workspace-backups.git
    wyrwood-directory: /run/wyrwood
    
    oidc:
      issuer: https://id.example.test
      client-id: menagerie
      redirect-url: https://menagerie.example.test/auth/callback
    
    templates:
      - id: sample-workspace-1
        name: Sample workspace 1.0
        address: gh:example-org/workspace-templates@1.0.0#sample-workspace
      - id: sample-workspace-2
        name: Sample workspace 2.0
        address: gh:example-org/workspace-templates@2.0.0#sample-workspace
        runner-image: ghcr.io/example-org/sample-workspace-runner:2.0.0
    
    registry-credentials:
      kv-mount: kv
      registries:
        - {registry: ghcr.io, path: menagerie/ghcr}
        - {registry: docker.io, path: menagerie/docker-hub}
    
    local-env: [TZ, LANG]
    
    job-limits:
      default: 2h
      operations:
        rebuild: 1h
      phases:
        initialize-command: 10m
        build: 45m
    
    alerts:
      webhook: https://ntfy.example.test/menagerie
      long-running: 45m
    
    auto-commit:
      every: 1h
    
    workspace-socket:
      folder: /srv/menagerie/sockets
      min-interval: 1m
    
    github-app:
      app-id: 123456
      transit-key: menagerie-github-app
      installations:
        example-org: 7890

    Workspaces

    workspace-root is the absolute path of the folder that holds the workspaces, the same path on the host and in the container. Every folder directly in it is a workspace, whether Menagerie created it or not, unless its name starts with a dot: a folder such as .shared belongs to the host. The path is never /. Menagerie finds a workspace’s container by the devcontainer.local_folder label and shows no other container.

    workspace-repository is the Git remote for backups. Each workspace is pushed, by force, to an orphan branch named after its folder. SSH remotes use the Wyrwood agent and the known hosts in /etc/ssh/ssh_known_hosts. An https remote on the GitHub App’s server uses an installation token.

    wyrwood-directory is where the Wyrwood consumer directory is mounted in the container, /run/wyrwood in the deployment example.

    Templates

    templates is the approved list; a manager can use only these. Each entry has a unique id (lower-case letters and digits, separated by single dashes), a display name, and a Toha address:

    • gh:<owner>/<repository>@<ref>#<path>, a GitHub repository over HTTPS, for public templates;
    • git@<host>:<path>.git@<ref>#<path> or ssh://git@<host>/<path>.git@<ref>#<path>, over SSH with the Wyrwood agent, for private templates;
    • https://<host>/<path>.git@<ref>#<path>, with a GitHub App token for private templates on its server.

    #<path> names the template’s folder in the repository and is optional.

    An entry’s optional runner-image is the image its hooks run in; see Runner images.

    Pin each entry to a tag. To move workspaces to a new template version, add an entry for that version; Upgrade then asks only the questions the new version adds and keeps the answers and local edits the workspace has, or every question with Ask every question again. The list can be empty.

    Runner images

    Template hooks and initializeCommand run in throwaway containers, one per command. A container mounts only the workspace folder, at its own path, and runs as the owner of the workspace root on Docker’s default bridge network, unable to gain privileges through setuid programs. An upgrade’s hooks mount nothing: they run in a copy of Toha’s candidate, the upgraded checkout Toha merges into the workspace afterwards, so they never write to the workspace folder themselves. It cannot reach the Docker socket, the Wyrwood sockets or Menagerie’s environment, and it is removed when its command ends.

    • A template’s hooks run in its runner-image, else the top-level runner-image.
    • initializeCommand runs in the top-level runner-image.
    • Without either, both run in Menagerie’s own image, which has sh, git, ssh and curl.

    The image needs the programs the hooks run; a hook runs its program as given, found on the image’s own PATH. Toha checks a template’s requires against Menagerie’s own image, so a template with its own runner image should list only programs both images have. Menagerie pulls a missing image, with registry-credentials when they name its registry. A hook gets no environment variables beyond its image’s own, and no SSH agent: a hook that fetches a private repository cannot authenticate.

    Identity provider

    oidc names the provider and Menagerie’s public client:

    KeyValue
    issuerThe provider’s issuer URL; its discovery document must name the same issuer.
    client-idThe public client’s id. There is no client secret.
    redirect-urlThe registered redirect URI, https://<host>/auth/callback. Its origin is the address browsers use; https makes every cookie Secure.
    scopesOptional. Must include openid. Without it Menagerie asks for openid profile groups.

    Only members of the group menagerie_managers get a session. Membership is read at sign-in from the ID token’s groups claim, or from userinfo when the ID token has none. Sessions live in Menagerie’s memory: a restart signs everyone out, and removing someone from the group takes effect at their next sign-in, or at once after a restart.

    Registry credentials

    Without registry-credentials every registry is anonymous. With it, Menagerie reads a registry’s username and password from OpenBao, through Wyrwood’s vault.sock, when a pull, build, feature or Compose service needs that registry.

    • kv-mount is a KV version 2 mount.
    • registries lists {registry, path}. registry is a host or host:port; docker.io, index.docker.io, registry-1.docker.io and https://index.docker.io/v1/ all name Docker Hub, and two entries cannot name one registry. path is the secret’s path in the mount.
    • The secret holds the string fields username and password.
    • The Wyrwood policy grants read on <kv-mount>/data/<path> for each entry.

    Menagerie sends no token and no namespace; Wyrwood adds the binding’s token and renews it. A registry no entry names is anonymous. When a credential cannot be read, the job fails with registry-credentials-unavailable and one of the reasons in Troubleshooting.

    GitHub App

    With github-app, Menagerie authenticates to GitHub with a GitHub App’s installation tokens, for repositories on the App’s server whose path is /<owner>/<repository> or /<owner>/<repository>.git:

    • Backups, with contents: write on the repository, when workspace-repository is such an https URL with no user information, query or fragment. A workspace-repository with a user name gets no token and no other credential.
    • Template fetches, with contents: read on the repository, from every https address of such a repository. A user name in the address does not opt out: the template service asks for the token by the address without it, and the fetch sends the token in its place.

    Every other URL keeps its own authentication: SSH URLs use the Wyrwood agent, and Menagerie gives other https URLs no credential. Both can be configured, for example backups and templates on GitHub through the App and templates on another host over SSH.

    KeyValue
    app-idThe App’s id.
    transit-keyThe Transit key, at the mount transit, that holds the App’s private key: letters, digits, _, . and -.
    installationsOptional. The App’s installation id by account name, compared in lower case. A repository whose owner is not listed has its installation looked up.
    serverOptional. The GitHub server’s https origin; without it, https://github.com. The API is https://api.github.com for GitHub, <server>/api/v3 for GitHub Enterprise Server.

    Install the App on the backup repository with Contents read and write, and on each private template repository with Contents read. Import its private key into OpenBao’s Transit engine as an RSA key, as Installation shows; the Wyrwood policy grants update on transit/sign/<transit-key>/sha2-256. For a server whose certificate a private authority signed, see Git hosts. Menagerie signs the App’s JWT through Wyrwood’s vault.sock, sending no token, and exchanges it for a token that covers one repository and one permission. Tokens stay in Menagerie’s memory until five minutes before they expire; nothing is written to disk. A push or fetch with a token follows no redirect: set workspace-repository to the repository’s current address. A failure names a fixed reason, never a token, a URL or GitHub’s answer, and the job console shows the remote’s own text with the URL and the token replaced by [redacted].

    Provisioning

    provisioning names what provisioning a workspace uses on this host.

    readiness-wait bounds each wait on a binding’s status, such as the wait for a container’s sockets. Absent, it is 2m. It times the host, never a person. A wait that passes it fails the job as provisioning-not-ready, with the status and the agent’s last error.

    Wyrwood operator key

    provisioning:
      wyrwood:
        controller: ssh://wyrwood-controller:2222
        host-key: SHA256:<wyrwood controller host-key>
        agent: sample-host
        operator-key: menagerie-operator

    provisioning.wyrwood lets Menagerie manage the bindings of workspaces. Menagerie needs Wyrwood 0.6.1 or later. Prepare the host first.

    • controller is the controller’s ssh://host[:port]. The port defaults to 22.
    • host-key is the controller’s SHA256 fingerprint, as wyrwood controller host-key prints it. Menagerie offers no key to a controller that presents another.
    • agent is the name of this host’s Wyrwood agent.
    • operator-key selects the operator key on ssh-agent.sock: its Transit key name, its public key or its SHA256 fingerprint. Menagerie offers the controller only this key, so the Git key is not recorded as pending.

    Each connection and each request has 30 seconds. An operator key with no approval, or a call outside its grant, fails the job with the operation and the container name, and an operator key with no approval also with the key’s fingerprint.

    Xolotl

    provisioning:
      xolotl:
        url: http://xolotl:8317
        key-secret:
          kv-mount: kv
          path: menagerie/xolotl

    provisioning.xolotl lets Menagerie issue and revoke the client key a workspace’s definition declares. Menagerie needs Xolotl 0.6.0 or later.

    • url is an http or https URL with a host and no user information, query or fragment.
    • key-secret names the OpenBao secret whose string field api_key holds Menagerie’s scoped key. kv-mount is a KV version 2 mount and path is the secret’s path in the mount.
    • The Wyrwood policy grants read on <kv-mount>/data/<path>.

    Menagerie reads the scoped key through Wyrwood’s vault.sock for each operation and stores it nowhere. The key goes only to url, and a redirect fails the job.

    Prepare two Xolotl policies, with one key policy for each host:

    # The policy of the workspaces' client keys on this host.
    name = "host-sample"
    description = "Client keys of this host's workspaces"
    scopes = ["proxy", "mcp"]
    providers = ["*"]
    # The policy of Menagerie's scoped key.
    name = "host-sample-provisioner"
    description = "Issues keys for this host's workspaces"
    scopes = ["keys"]
    key_policies = ["host-sample"]

    Give the first policy the providers, accounts and MCP servers the workspaces use. Xolotl answers 403 when a listed policy grants any scope other than proxy or mcp, and across instances a policy change can take up to 60 seconds to apply. Issue the scoped key from the second policy with xolotl keys issue --policy host-sample-provisioner --name menagerie, and put it in the secret that key-secret names.

    Menagerie issues a client key with no expiry, and a repeat leaves a key that exists as it is. It sends a request up to three times, one second apart, while Xolotl answers 409 or 500. Each request has 30 seconds.

    Local environment

    A devcontainer definition reads Menagerie’s own environment through ${localEnv:NAME} (and ${env:NAME}). local-env lists the variables it may read; without it, it reads none. Each entry is a variable name: letters, digits and underscores, not starting with a digit, and each name once.

    • A reference to any other variable resolves to its default value, such as fallback in ${localEnv:NAME:fallback}, or to an empty string, as an unset variable does. The job log notes each one: localEnv NAME is not in local-env; it resolves to its default value.
    • Compose interpolation of ${VAR} in a workspace’s Compose files sees only the listed variables.
    • initializeCommand gets only the listed variables, beside its runner image’s own environment. Its PATH is always the image’s: a listed PATH is readable through ${localEnv:PATH} but never reaches initializeCommand.
    • build.args reach the build as written: devcontainer-builder does not substitute variables in them yet.

    Job time limits

    Without job-limits a job runs until it ends. With it, a job past a limit is cancelled and fails as timed-out, and its workspace takes the next job once the cancelled work has stopped.

    • default limits every operation.
    • operations gives start, stop, rebuild, create, upgrade, commit, apply-branch or delete its own limit, which replaces default.
    • phases limits each stay in a phase, inside any operation: read-definition, issue-key, bind, initialize-command, build, unbind, remove-container, revoke-key, remove-folder, create-container, wait-for-sockets, up, verify, retire, stop, interview, initialize-repository, generate, host-prerequisites, fetch, plan, apply, commit, backup-local-changes or backup. Template hooks run inside generate and apply; a hook still running when its job is cancelled has its container killed and removed before the job ends. A template fetch cannot be interrupted, so it runs until the remote or the network ends it; if the job has ended by then, it writes nothing to the workspace folder.

    A duration is whole hours, minutes and seconds, such as 2h, 45m or 1h30m. A key without a value, such as rebuild: or default: "", is an error, also when an alias or a merge key gives it; leave the key out for no limit. Limits count only the time Menagerie works: while a job waits for answers to an interview, no limit counts and the interview has no expiry. The job console shows the limit that ends first and the time left.

    Alerts

    With alerts, Menagerie posts one JSON message to webhook for every job that fails, timed-out jobs included. With long-running, it also posts one when a job’s running time passes that duration. Ended and succeeded jobs send nothing.

    {
      "event": "job-failed",
      "jobId": "4f1c...",
      "workspace": "sample-space",
      "operation": "rebuild",
      "phase": "build",
      "state": "failed",
      "error": { "kind": "timed-out", "message": "the build phase passed its 45m limit" },
      "console": "https://menagerie.example.test/?job=4f1c..."
    }

    event is job-failed or job-long-running; a long-running alert has "state": "running" and "error": null. console opens the job’s console at the origin of oidc.redirect-url. The body suits a generic webhook such as ntfy, Slack through a relay, or Dagu.

    Menagerie makes one attempt per alert with a 10 second limit and does not follow redirects. Alerts are posted one at a time in the order they were raised, so a job’s job-long-running alert arrives before its job-failed alert. An alert never delays or fails a job: up to 64 alerts wait for delivery, and an alert raised while 64 wait is dropped with a log line. At shutdown Menagerie waits at most 10 seconds for waiting alerts. A failed delivery writes one line to the container’s log that names only what failed: timeout, transport error, redirect refused or status 503, for example. The webhook URL can carry a token, so Menagerie never writes it to a log, an event or an error. A configuration error names the field and what it expects, never the value of the webhook or of a duration.

    Automatic commits

    With auto-commit, Menagerie checks every workspace each every, the first time one interval after it starts. A workspace with uncommitted changes and no job in progress gets a commit job: every change is committed, as git add --all and a commit would, with the message chore(menagerie): automatic commit, and the workspace’s branch is backed up. A workspace with a job in progress is skipped until the next check. Files the workspace’s ignore rules exclude, and repositories cloned inside it, are not committed. The ignore rules are the workspace’s .gitignore files, its .git/info/exclude, and the file its repository’s own core.excludesFile names when that file is a regular file of at most 1 MiB inside the workspace folder, with symbolic links resolved. Any other exclude file, elsewhere or larger, is skipped with a note in the job’s console, and the files it excludes can be committed. Menagerie’s own Git configuration does not apply. Without auto-commit nothing is committed on a schedule.

    Workspace sockets

    With workspace-socket, every workspace’s devcontainer can commit and back up its own workspace with workspace-commit, as Usage shows. Menagerie keeps one folder per workspace in folder, with a Unix socket and the client, and mounts it read-only at /run/menagerie in that workspace’s devcontainer. The socket only knows its own workspace, and needs no token.

    • folder is an absolute folder that is mounted at the same path on the host and in Menagerie’s container, like the workspace root. It must exist before Menagerie starts and belong to the owner of the workspace root, the user the services run as; Menagerie sets its mode to 0700. It must not hold a comma, it must not itself be a symbolic link, and every folder above it must belong to root or that user and be writable by no other user unless it is sticky, as /tmp is. A symbolic link above it, such as /var/run to /run, is followed. It must be neither inside nor around the workspace root, the Wyrwood directory or the candidate folder; the workspace root is also compared with both paths’ symbolic links resolved.
    • min-interval is the shortest time between two commits of one workspace through its socket, 1m by default; 0 allows any number. A request that comes sooner is refused with the time to wait, so a loop in a devcontainer cannot fill its backup branch.

    A devcontainer gets the socket when its container is created or rebuilt. A start keeps a container that was created without the socket as it is, so it is not recreated, and the job’s console notes that the socket attaches at the next rebuild. A start that recreates the container for another reason, such as a changed containerEnv, also creates it without the socket, and the console says so; the socket attaches at the next rebuild. A Compose workspace gets the socket on its primary service only.

    Each workspace’s socket takes at most 32 connections at once, and all sockets together 256. A request has 10 seconds for its header and 10 for its body, and an idle connection is closed after 60 seconds; the wait for the commit job is bounded only by the commit job time limit.

    Listen address and template service

    listen is the HTTP address, :8080 by default. template-service.socket is the template service’s Unix socket, /run/menagerie/template.sock by default. When you change either, set MENAGERIE_HEALTH_URL or MENAGERIE_TEMPLATE_SOCKET in the container’s environment to match, so the health check asks the right place. A changed socket must be in a folder only the services’ user can enter.

    template-service.candidate-folder is the template service’s temporary folder, /var/lib/menagerie/candidates by default, where Toha builds an upgrade’s candidate. Menagerie runs an upgrade’s hooks only against a folder inside it. It must be an absolute folder other than /, neither inside workspace-root nor containing it, and only the services’ user may enter it; the template service creates it when it is missing. Its temporary entries are in the subfolder .menagerie-work. At each start, unless another template service uses that subfolder, the service removes what a killed upgrade left directly inside it: entries of its own user named .tmp and six letters or digits, and folders named .menagerie-copy- and a number. It names each one in its log and leaves everything else.