Skip to main content

Latchkey integration plugin for mngr - manages a shared latchkey gateway and per-agent permissions

Project description

mngr-latchkey

Latchkey gateway management for mngr.

This package owns the lifecycle of a single shared latchkey gateway subprocess and the per-agent state that points the gateway at each agent's own permissions file. It ships both as a Python library and as a mngr CLI plugin that registers the mngr latchkey command group.

CLI

Once imbue-mngr-latchkey is installed, mngr discovers the plugin via the standard entry-point mechanism and exposes:

mngr latchkey forward            # long-running supervisor: gateway + reverse tunnels
mngr latchkey create-agent-env   # emit LATCHKEY_* env vars + opaque permissions handle as JSON
mngr latchkey link-permissions   # swing the opaque handle's symlink to the canonical host path
mngr latchkey admin-jwt          # mint a wildcard permissions-override JWT for the gateway
mngr latchkey gateway-info       # print the running gateway's URL + listen password as JSON

mngr latchkey forward spawns the shared gateway eagerly on startup and stops it on SIGINT/SIGTERM (coupled lifetime). Any in-flight agents lose their gateway endpoint until the next mngr latchkey forward is started; the per-host permissions files survive across restarts.

Wiring a new agent using the CLI interface

# In one terminal, leave the supervisor running for the lifetime of the agents.
export MNGR_LATCHKEY_DIRECTORY=~/.minds/latchkey
mngr latchkey forward

# In another terminal, per host:
export MNGR_LATCHKEY_DIRECTORY=~/.minds/latchkey
mngr latchkey create-agent-env > /tmp/lk.json
OPAQUE_PATH=$(jq -r .opaque_permissions_path /tmp/lk.json)
HOST_ENV_ARGS=$(jq -r '.env | to_entries[] | "--host-env \(.key)=\(.value)"' /tmp/lk.json)

# Substitute your preferred mngr create invocation here. The latchkey
# env is passed via --host-env so every agent on the new host inherits
# the same gateway wiring.
CREATED=$(mngr create my-template $HOST_ENV_ARGS --format json)
HOST_ID=$(echo "$CREATED" | jq -r .host_id)
AGENT_ID=$(echo "$CREATED" | jq -r .agent_id)

# Finalize the opaque permissions handle: swing its symlink to the
# canonical host-keyed permissions path.
mngr latchkey link-permissions --host-id "$HOST_ID" --opaque-path "$OPAQUE_PATH"

# Register this agent for the host so it can reach the Minds API proxy.
# The baseline rule rejects every ``/minds-api-proxy/api/v1/agents/<id>/...``
# request whose ``<id>`` is not in the host's allowed-agent enum, so
# every minds agent that wants to call the Minds API must be registered
# here. Idempotent: re-running for an already-registered agent is a no-op.
mngr latchkey register-agent --host-id "$HOST_ID" --agent-id "$AGENT_ID"

Settings

[plugins.latchkey]
directory = "~/.mngr/latchkey"   # default
latchkey_binary = "latchkey"     # default; resolved via PATH

Both fields are overridable via the matching env vars (MNGR_LATCHKEY_DIRECTORY, MNGR_LATCHKEY_BINARY) and per-invocation CLI flags (--latchkey-directory, --latchkey-binary). Precedence is CLI flag > env var > settings.toml > built-in default.

Embedding

Embedders (such as the minds desktop client) typically want a single detached mngr latchkey forward supervisor that survives embedder restarts and adopts the existing one instead of double-spawning. The :class:LatchkeyForwardSupervisor does exactly that:

from imbue.mngr_latchkey.forward_supervisor import LatchkeyForwardSupervisor

supervisor = LatchkeyForwardSupervisor(
    mngr_binary="/path/to/mngr",          # default: ``mngr`` on PATH
    latchkey_binary="/path/to/latchkey",  # default: ``latchkey`` on PATH
    latchkey_directory=root_dir,
)
supervisor.ensure_running()  # idempotent; spawns or adopts as needed
# ... do whatever the embedder does ...
# Optional: ``supervisor.stop()`` to terminate the detached process and
# tear down the gateway. Omitting this leaves the supervisor running
# detached, which is what minds does so the gateway survives a
# desktop-client restart.

Python API

Every CLI subcommand is a thin wrapper around the library; the library remains importable for embedders such as the minds desktop client.

from imbue.mngr_latchkey.core import Latchkey
from imbue.mngr_latchkey.agent_setup import (
    prepare_agent_latchkey,
    finalize_host_permissions,
)
from imbue.mngr_latchkey.discovery import (
    LatchkeyDiscoveryHandler,
    LatchkeyDestructionHandler,
)
from imbue.mngr_forward.ssh_tunnel import SSHTunnelManager

latchkey = Latchkey(
    latchkey_binary="/path/to/latchkey",  # default: "latchkey" on PATH
    latchkey_directory=root_dir,
)
latchkey.initialize()

# (a) Pre-create env vars + opaque permissions handle for a new host.
setup = prepare_agent_latchkey(latchkey, is_tunneled=True)
# setup.env: LATCHKEY_GATEWAY[_PASSWORD,_PERMISSIONS_OVERRIDE,_DISABLE_COUNTING]
# setup.opaque_permissions_path: pass to finalize_host_permissions later

# ... mngr create returns the canonical host id ...

# (b) Point the opaque handle at the canonical host permissions path.
finalize_host_permissions(latchkey, setup.opaque_permissions_path, host_id)
# Raises LatchkeyStoreError on failure -- callers decide whether to abort
# or just surface a warning.

# (c) Plug the discovery and destruction handlers into your agent
# discovery stream so reverse tunnels are opened on discovery and
# closed on destruction.
tunnel_manager = SSHTunnelManager()
tunnel_manager.start_reverse_tunnel_health_check()
on_discovered = LatchkeyDiscoveryHandler(
    latchkey=latchkey, tunnel_manager=tunnel_manager, concurrency_group=cg
)
on_destroyed = LatchkeyDestructionHandler(tunnel_manager=tunnel_manager)

The latchkey_directory is used both as the upstream LATCHKEY_DIRECTORY for spawned latchkey subprocesses and as the root of this package's own metadata subdirectory (<latchkey_directory>/mngr_latchkey/, accessible via Latchkey.plugin_data_dir).

Permissions config

The package owns the latchkey_permissions.json schema (a subset of detent's rule format). Per-host edits go through the gateway's bundled permissions extension (see below); only the deny-all default, the admin file, and the per-agent opaque baseline are written directly via imbue.mngr_latchkey.store.save_permissions.

Gateway HTTP extensions

mngr latchkey forward drops three .mjs extensions into <latchkey-directory>/extensions/. All expose plain HTTP endpoints on the gateway's listen port and authenticate the caller via two headers:

  • X-Latchkey-Gateway-Password: <password> -- the gateway listen password from mngr latchkey gateway-info.
  • X-Latchkey-Gateway-Permissions-Override: <jwt> -- a JWT minted for the permissions file you want the gateway to evaluate the request against. For full access to both extensions, use the JWT from mngr latchkey admin-jwt.

A shell client would typically wire these up once:

ADMIN_JWT=$(mngr latchkey admin-jwt)
eval "$(mngr latchkey gateway-info | jq -r '@text "GATEWAY_URL=\(.url); GATEWAY_PASSWORD=\(.password)"')"
auth=(-H "X-Latchkey-Gateway-Password: $GATEWAY_PASSWORD" -H "X-Latchkey-Gateway-Permissions-Override: $ADMIN_JWT")

permission-requests extension

A pending-permission queue. Agents submit a request when they hit a blocked service; UIs (the minds desktop client, your own front-end) consume the stream and approve/delete on resolution.

  • POST /permission-requests with body {"agent_id": "...", "rationale": "...", "type": "...", "payload": {...}}. Two type values are accepted:

    • "predefined" -- detent scope/permission grant, with payload {"scope": "...", "permissions": ["...", ...]}.
    • "file-sharing" -- single-file access through the minds-api-proxy extension, with payload {"path": "<absolute-path>"}. The path must be absolute and free of .. segments.

    The extension generates a request_id server-side, stores the caller-supplied fields plus the target permissions.json (taken from the extension context) and a precomputed effect ({rules?, schemas?}) that an approval would splice into target, and returns the full persisted record. Available to agents.

  • GET /permission-requests returns the current queue as newline-delimited JSON. Each line carries the full persisted shape. Add ?follow=true to keep the connection open and stream every newly-POSTed request as it arrives. Available to the admin.

  • POST /permission-requests/approve/<request_id> approves the named request: the extension reads it, splices its effect into its target permissions.json (creating the file if missing, merging rules by scope key and schemas by name), then removes the pending request file. Returns 200 with {request_id, target, applied} where applied is the freshly-rewritten permissions file. Available to the admin.

  • DELETE /permission-requests/<request_id> removes a single pending request without applying its effect. UIs call this on deny so a fresh ?follow=true consumer never sees the resolved request again. Available to the admin.

Pending requests are stored as one JSON file per request under <latchkey-directory>/permission_requests/v2/. The v2 segment is the on-disk schema version; future shape changes get a new directory rather than trying to migrate files in place.

minds-api-proxy extension

Transparent HTTP reverse proxy from the gateway to an embedder-supplied "Minds API" base URL.

  • ANY /minds-api-proxy forwards to <minds-api>/.
  • ANY /minds-api-proxy/<rest>... forwards to <minds-api>/<rest>..., preserving the inbound method, query string, headers (minus hop-by-hop entries and the gateway-internal password / permissions-override headers), and body. The upstream response status, headers, and body stream straight back.

The upstream base URL is read from the LATCHKEY_EXTENSION_MINDS_API_URL env var on every request. If the var is unset/empty/unparseable the proxy responds 503 with a JSON error body. There is no in-process cache to invalidate: an embedder that needs to repoint the proxy at a new upstream simply respawns the gateway (or the mngr latchkey forward supervisor that owns it) with a fresh value for the env var.

The proxy authenticates to the upstream Minds API on behalf of the agent. When LATCHKEY_EXTENSION_MINDS_API_KEY is set, the proxy overwrites the inbound Authorization header with Bearer <LATCHKEY_EXTENSION_MINDS_API_KEY> before forwarding. Agents therefore never see the key, and an agent that tries to spoof an Authorization header has its value dropped on the floor. When the env var is unset, the inbound Authorization value is forwarded unchanged (useful for tests / local fixtures that do not bother stubbing the key; the upstream will simply 401 the request).

Other than the Authorization overwrite, the extension performs no authentication of its own beyond the gateway's normal permission check (against the synthetic latchkey-self.invalid URL). Restricting which paths an agent can reach through the proxy is therefore a job for the agent's latchkey_permissions.json.

permissions extension

Reads and edits a detent permissions file at a caller-supplied path. The gateway is launched with the environment variable LATCHKEY_EXTENSION_PERMISSIONS_ROOT pointing at this package's data directory; any path query parameter that resolves outside that root is rejected with HTTP 403.

  • GET /permissions?path=<file> returns the full permissions file.
  • GET /permissions/available returns the full permission catalog as a JSON object keyed by raw service name. Each value is an array of scope entries (a single service may expose more than one scope), each with the shape {"scope": "<schema_name>", "display_name": "...", "description": "...", "permissions": [{"name": "<schema_name>", "description": "..."}, ...]}. The scope-level description and each permission's description carry detent's per-schema $comment summaries (both optional).
  • GET /permissions/available/<service_name> returns the permission catalog entries for <service_name> (e.g. slack, google-gmail) as an array, using the same value shape, or 404 if the service is unknown. Both endpoints are backed by a services.json file (keyed by raw service name) that ships alongside the extension; the path query parameter is not consulted.
  • GET /permissions/rules?path=<file>&rule_key=<scope> returns the rule for <scope>, or 404 if absent.
  • POST /permissions/rules?path=<file>&rule_key=<scope> with a JSON body of permission-schema names (["any"], ["slack-read-all", ...], ...) adds or replaces the rule for <scope>. Everything in the file other than the matching rule is preserved verbatim.
  • DELETE /permissions/rules?path=<file>&rule_key=<scope> removes the named rule.

The services.json catalog is generated from detent's built-in request schemas; do not edit it by hand. Regenerate it against a detent checkout with:

uv run python libs/mngr_latchkey/scripts/generate_services_json.py \
  --detent-root /path/to/detent

Display names and the service ordering are editorial metadata detent does not carry; they live as curated constants in that script.

A typical end-to-end shell flow:

# Stream pending requests as they come in.
curl -N "${auth[@]}" "$GATEWAY_URL/permission-requests?follow=true"

# Grant the agent slack-read-all on its host's permissions file.
HOST_PERMS=$MNGR_LATCHKEY_DIRECTORY/mngr_latchkey/hosts/$HOST_ID/latchkey_permissions.json
curl -X POST "${auth[@]}" \
  -H "Content-Type: application/json" -d '["slack-read-all"]' \
  "$GATEWAY_URL/permissions/rules?path=$HOST_PERMS&rule_key=slack-api"

# Clear the pending request now that it has been resolved.
curl -X DELETE "${auth[@]}" "$GATEWAY_URL/permission-requests/$REQUEST_ID"

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

imbue_mngr_latchkey-0.1.0.tar.gz (157.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

imbue_mngr_latchkey-0.1.0-py3-none-any.whl (95.2 kB view details)

Uploaded Python 3

File details

Details for the file imbue_mngr_latchkey-0.1.0.tar.gz.

File metadata

  • Download URL: imbue_mngr_latchkey-0.1.0.tar.gz
  • Upload date:
  • Size: 157.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for imbue_mngr_latchkey-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8167afebf612933dc09d758d163560c9e6fccebd4e7223e458df44c4c92d034e
MD5 6f74da578aa1c1344eb372a1a735df11
BLAKE2b-256 b0f9b668e86f1edaab7a97808aefaff251dac818fc5a78a27e0cfeed6153aee4

See more details on using hashes here.

Provenance

The following attestation bundles were made for imbue_mngr_latchkey-0.1.0.tar.gz:

Publisher: publish.yml on imbue-ai/mngr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file imbue_mngr_latchkey-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for imbue_mngr_latchkey-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 53b1344959827eb0c23c05b7498ac35a79dab93a0f18bc5f71a653ad2df74be1
MD5 cfcce64535b014fe8419fe8b24b58475
BLAKE2b-256 4ffa614277c90033d8f89742e646dec63908c935214c9edb64a87171aea9c186

See more details on using hashes here.

Provenance

The following attestation bundles were made for imbue_mngr_latchkey-0.1.0-py3-none-any.whl:

Publisher: publish.yml on imbue-ai/mngr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page