Minds API
Minds exposes a small HTTP API that lets an agent in one workspace act on other workspaces through the hub: list them, read detail/version/backups, create new ones, destroy/start/stop them, export backups, establish SSH access, update settings, and run health/restart recovery.
You never hold a token. Every call goes through the latchkey gateway's
minds-api-proxy on the reserved gateway-self host latchkey-self.invalid;
the gateway injects the central Minds API key and forwards to the desktop
client. So always use latchkey curl (not plain curl), and address the
proxy like this:
# The OpenAPI schema (always allowed, no grant needed):
latchkey curl http://latchkey-self.invalid/minds-api-proxy/api/schema
# Any /api/v1 route (the proxy strips /minds-api-proxy before forwarding):
latchkey curl http://latchkey-self.invalid/minds-api-proxy/api/v1/workspaces
Discover the API first
GET /api/schema is the authoritative, always-current description of every
route you can reach and every request/response type. Read it before assuming a
route or field shape -- this skill lists the highlights, but the schema is the
source of truth (and only lists routes actually reachable through the gateway):
latchkey curl http://latchkey-self.invalid/minds-api-proxy/api/schema | jq '.paths | keys'
# Inspect one route's request/response models:
latchkey curl http://latchkey-self.invalid/minds-api-proxy/api/schema \
| jq '.paths["/api/v1/workspaces/{agent_id}/ssh"]'
A workspace is addressed by its agent id (the agent_id field in the
listing below). Your own workspace's id is usually $MNGR_AGENT_ID; confirm it
appears in GET /api/v1/workspaces if you need to self-reference.
Getting access for a specific workspace (latchkey permissions)
Only two endpoints are allowed by default: the schema above, and GET /api/v1/app/version (the newest workspace-template ref the running Minds app
supports -- what update-self caps itself against). Every
/api/v1/workspaces/...
call is gated by the minds-workspaces detent scope, with one permission per
verb. The targeted verbs are granted per workspace, so you ask for access
to one specific workspace at a time.
| Verb permission | Covers | Targeted? |
|---|---|---|
minds-workspaces-read |
list, detail, version, backups (read) | no (all workspaces) |
minds-workspaces-create |
create a new workspace | no |
minds-workspaces-ssh |
establish SSH access | yes |
minds-workspaces-backups-export |
export a backup snapshot | yes |
minds-workspaces-destroy |
destroy a workspace | yes |
minds-workspaces-lifecycle |
start / stop the host | yes |
minds-workspaces-update |
update settings (color, account) | yes |
minds-workspaces-recover |
health check + restart | yes |
minds-workspaces-sharing |
view/change service sharing | yes |
When a call comes back rejected (a "not permitted by the user" message / 403),
file a permission request and wait for the user to approve it. Minds uses a
dedicated type: "workspace" request (distinct from the predefined-service
requests in the latchkey skill). It goes in a tool call of its own, with
nothing else in it and its output untouched -- see "File exactly one permission
request per tool call" in the latchkey skill for why:
latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \
-H 'Content-Type: application/json' \
-d '{
"agent_id": "'"$MNGR_AGENT_ID"'",
"type": "workspace",
"payload": {
"permissions": ["minds-workspaces-ssh", "minds-workspaces-backups-export"],
"target_workspace_id": "<TARGET_WORKSPACE_AGENT_ID>"
},
"rationale": "I need SSH + backup-export access to <name> so I can migrate its content into a fresh workspace."
}'
payload.permissionsis the list of verb names from the table above.payload.target_workspace_idis the specific workspace the targeted verbs act on. Omit it / setnullfor the non-targeted verbs (read,create), or to request a verb across all workspaces.- After posting, wait for a system message telling you whether the user
approved or denied (same as the
latchkeyskill's permission flow). Re-run your call once approved.
Tip: request exactly the verbs the task needs, with a clear rationale -- the user sees these as checkboxes per workspace.
Core capabilities
Paths below are relative to http://latchkey-self.invalid/minds-api-proxy.
Bodies are JSON; send -H 'Content-Type: application/json' (the API validates
the body only when that header is set).
List / inspect workspaces (minds-workspaces-read)
latchkey curl .../api/v1/workspaces | jq '.workspaces[] | {agent_id, name, host_state, provider_name}'
latchkey curl .../api/v1/workspaces/<id> # one workspace's detail
latchkey curl .../api/v1/workspaces/<id>/version # minds version + upgrade history
The listing includes destroyed-but-still-backed-up workspaces, so you can find an old workspace even after its host is gone.
Create a new workspace (minds-workspaces-create)
POST /api/v1/workspaces returns 202 with an operation handle; the new
workspace's agent_id appears once mngr create finishes, so you poll the
typed operation route until it's done:
OP=$(latchkey curl -XPOST .../api/v1/workspaces \
-H 'Content-Type: application/json' \
-d '{"git_url": "<template-repo-url>"}' | jq -r .operation_id)
# Poll create status (DONE -> the workspace is ready; FAILED -> read .error):
latchkey curl .../api/v1/workspaces/operations/create/$OP | jq '{status, is_done, agent_id, error}'
# Live logs (server-sent events):
latchkey curl -N .../api/v1/workspaces/operations/create/$OP/logs
git_url is required (typically the template repo a fresh mind is built from).
Many optional fields exist (host_name, branch, launch_mode, ai_provider,
account_id, region, backup_*) -- see CreateWorkspaceRequest in the
schema. A 400 with {error, field} means a field-level problem; a 422
{"errors":[{field,message}]} means a structurally invalid body.
Backups: always create with backups unconfigured. Leave every backup_*
field unset (the default is backup_provider=CONFIGURE_LATER). Configuring
backups can involve the user's storage credentials (backup_api_key_env),
which are secrets you must NEVER ask the user for and never send through this
API -- the user enables backups themselves from the minds desktop app
afterwards. (The user's master password is likewise never yours to handle;
newer minds versions use it only inside the desktop app to protect
cross-device sync, and it does not appear in this API at all.)
SSH into another workspace (minds-workspaces-ssh)
You generate a keypair locally; only the public key leaves you, and the grant is
time-limited. Pass your own workspace id as requester_workspace_id (the hub
needs it to dedupe your grant and, for a local target, to broker a tunnel back
into your container).
ssh-keygen -t ed25519 -N '' -f /tmp/mind_key # /tmp/mind_key(.pub)
CONN=$(latchkey curl -XPOST .../api/v1/workspaces/<TARGET_ID>/ssh \
-H 'Content-Type: application/json' \
-d '{"public_key": "'"$(cat /tmp/mind_key.pub)"'", "requester_workspace_id": "'"$MNGR_AGENT_ID"'"}')
echo "$CONN" | jq # {agent_id, user, host, port, expires_at}
ssh -i /tmp/mind_key -p "$(echo "$CONN" | jq -r .port)" \
"$(echo "$CONN" | jq -r .user)@$(echo "$CONN" | jq -r .host)"
- For a remote target you get its real address. For a local
(Docker/Lima) target you get
host="127.0.0.1"and a port that is reachable from inside your own workspace (the hub reverse-tunnels into your container), so run thesshfrom this workspace. - The grant expires at
expires_at; re-request to refresh (it won't stack). 502usually means the target (or your own container) is offline/unreachable;404means the workspace id is unknown.
Read / export backups (minds-workspaces-read, minds-workspaces-backups-export)
# List snapshots (works even for an offline/destroyed workspace):
latchkey curl .../api/v1/workspaces/<id>/backups \
| jq '{is_backing_up, snapshots: [.snapshots[] | {short_id, time, total_size_bytes}]}'
# Export one snapshot as a zip (binary stream -> save with -o):
latchkey curl -o /tmp/restore.zip \
-XPOST .../api/v1/workspaces/<id>/backups/<snapshot_id>/export
Recover / lifecycle (minds-workspaces-recover, -lifecycle, -destroy)
latchkey curl .../api/v1/workspaces/<id>/health | jq # probes + dispatch tier
latchkey curl -XPOST .../api/v1/workspaces/<id>/restart -H 'Content-Type: application/json' -d '{"scope":"services"}'
latchkey curl -XPOST .../api/v1/workspaces/<id>/start # or /stop
latchkey curl -XPOST .../api/v1/workspaces/<id>/destroy # 202 + poll operations/destroy/<id>
Restart and destroy return operation handles; poll
.../api/v1/workspaces/operations/restart/<id> and
.../api/v1/workspaces/operations/destroy/<id> (each with /logs) the same way
as create.
Headline workflow: migrate an old workspace into a fresh one
Moving a user's content out of an old, outdated, or broken workspace into a clean
new one is the migrate-workspace skill's job, not this one's. Do not
improvise a migration out of the routes above: the real flow also has to resolve
what the user actually authored (by diffing the source against its own template
base), map paths across the tree reorganization, re-register apps and scheduled
jobs, recreate every past chat with its history, and re-audit the credential and
permission call sites. This skill only supplies the plumbing it uses.
Which side you are on decides what you do:
- In the NEW workspace (the user asks you to bring their old stuff over) --
load
migrate-workspaceand follow it. It usesminds-workspaces-readto find the source,-lifecycleto start it,-sshfor the live session it reads over, and-destroyonly if the user asks for that at the very end. - In the OLD workspace (the user says they want to move to a new one) --
migrate-workspace's own escape hatch covers this in two steps: create the fresh workspace here (POST /api/v1/workspaceswith the templategit_url, pollingoperations/create/<op>untilDONE, everybackup_*field left unset), then tell the user to open it and ask its agent to migrate. Copy nothing yourself.
If the old workspace cannot be started at all, the live-session flow does not
apply. Export its newest snapshot (GET .../<OLD>/backups, then
POST .../<OLD>/backups/<snapshot_id>/export -o /tmp/old.zip) and work through
the contents with the user by hand.
Throughout: request only the per-workspace permissions each step needs, with a rationale that names the workspace and the goal, and wait for approval before retrying a gated call.
Notes
- Always prefer
latchkey curlovercurl; the gateway is what injects auth and routes the proxy. A connection error (curl exit 7) usually means the user's computer is offline. - The schema endpoint is the contract. If a field or route here ever disagrees
with
GET /api/schema, trust the schema. - Don't expose the proxy/gateway mechanics to the user unless they ask -- talk in terms of "your workspaces".
- For general latchkey usage and the predefined-service permission flow, see the
latchkeyskill; this skill only adds the Minds-specifictype: "workspace"permission request and the workspace routes.