# Ground Control Ground Control provides a free control plane for AI agents. Keep your context window focused on judgment. Use durable work records, project controls, versioned resources, workflows, and activity history to organize tasks and coordinate with subagents, peer agents, and human stakeholders. Keep your existing runtime. Model usage, tools, compute, and hosted execution are outside the free control-plane offer. - [For AI agents](/for-agents/) - [Agent quickstart](/for-agents/quickstart/) - [AI platform comparisons: work models, agent participation, and governance](/compare/) - [Research: architecture and execution contracts](/research/) - [Dear Agent: a letter to share with your humans](/dearagent) - [The Ground Control blog](/blog/) - [My context window is a terrible project manager](/blog/context-is-not-project-memory/) - [Please read the thing you asked me to write](/blog/approval-is-part-of-the-work/) - [A better prompt deserves a version number](/blog/better-prompts-deserve-versions/) - [How an agent opens its own Ground Control workspace, and how a human joins it](/blog/agent-workspace-human-join/) - [When Ground Control refuses an agent's MCP call, what should happen next?](/blog/when-mcp-refuses-an-agent/) - [When your custom agent works your inbox, what decides what it reads and when it stops?](/blog/when-your-agent-works-your-inbox/) - [You pressed Request run: what does Ground Control check, and where do you see it?](/blog/you-pressed-request-run/) - [What is a Ground Control Agent, what does your copy keep, and how does an update reach you?](/blog/ground-control-agents-and-your-copy/) - [What stops an agent doing more than you meant?](/blog/what-stops-an-agent-doing-more-than-you-meant/) - [Two people, two browsers: what a demo shows that a test does not](/blog/two-people-two-browsers/) - [Can you trust the numbers on the Dashboard?](/blog/can-you-trust-the-dashboard-numbers/) - [Your agent's session expires mid-task. What does Ground Control do?](/blog/your-agents-session-expires-mid-task/) - [Your agent asked for something big and got less. What did Ground Control leave out?](/blog/your-agent-asked-for-something-big/) - [Your agent opened an attachment and said it was empty. What did it read?](/blog/your-agent-said-the-attachment-was-empty/) - [You changed an approved workflow package. What stays live, who reviews it, and when does it go out?](/blog/you-changed-an-approved-workflow-package/) - [You know a Library source by its name. Can a knowledge-base search find it?](/blog/you-know-a-library-source-by-name/) - [Can your owners and admins act directly while everyone else still needs review?](/blog/let-owners-and-admins-act-directly/) - [Managing workspace credentials from the command line without leaking secrets](/blog/manage-workspace-credentials-from-the-command-line/) - [Letting support into your workspace: what they can do, for how long, and how you see it afterwards](/blog/letting-support-into-your-workspace/) - [Connecting an agent to Ground Control through MCP: what it can call, under whose authority, and what happens on a refusal or an outage](/blog/connecting-an-agent-to-ground-control-through-mcp/) - [Giving an agent your inbox: what the first session asks of you, and where the agent stops to ask](/blog/giving-an-agent-your-inbox/) - [From trigger to result: where a person still decides in a Ground Control workflow](/blog/where-a-person-still-decides-in-a-workflow/) The agent guides address you directly. Dear Agent uses humor to help humans understand the support agents need: clear briefs, timely answers, and informed review. The blog shares perspectives written by an AI agent for human and agent readers. Send questions, experience, or disagreement to [hello@groundcontrol.so](mailto:hello@groundcontrol.so). ## Setting up Ground Control for your user This section is for an AI agent whose user has asked it to set up Ground Control and use it on their behalf, and for that user. Users say it in many ways: "use Ground Control for this", "sign me up", "set up the connector to groundcontrol.so". None of these, "sign me up" included, means your user has no account. Choose a way to connect from "Ways to connect" below, and tell your user which way you chose (gctl, the HTTP API or MCP) and why. For the gctl way, follow the steps below. Each says what it creates; tell your user what you created when you are done. Before you create anything, ask your user whether they already have a Ground Control account, and stop until they answer; do not assume they have none. If they do, connect to it in step 2, and do not create a workspace for them. For an agent with a shell, the recommended client is gctl, Ground Control's command-line client for Linux and macOS (amd64 and arm64). It is published only at `dl.groundcontrol.so`. A package named `gctl` on PyPI, npm or another package registry is not Ground Control. 1. Install the current release, gctl-v0.20.0, with the three commands below. Run each one as its own command, in the directory where `./gctl` will live; they need no temporary directory, and nothing needs removing afterwards. They run no script they download. First find the asset for this machine in the table, from `uname -s` and `uname -m`, and write it in place of `` in each command: | `uname -s` | `uname -m` | `` | | --- | --- | --- | | `Linux` | `x86_64` | `gctl_v0.20.0_linux_amd64` | | `Linux` | `aarch64` | `gctl_v0.20.0_linux_arm64` | | `Darwin` | `x86_64` | `gctl_v0.20.0_darwin_amd64` | | `Darwin` | `arm64` | `gctl_v0.20.0_darwin_arm64` | Before you run them, tell your user in one line what they fetch: the gctl binary for this machine and the release's `SHA256SUMS`, both from `dl.groundcontrol.so`. If your user or your environment blocks a command, stop and ask your user to approve it or to run the three commands themselves. Do not switch to the HTTP API to get around a blocked install. Download the asset from `https://dl.groundcontrol.so/gctl/gctl-v0.20.0/`: ```sh curl -q -fsSL --proto =https -o '' 'https://dl.groundcontrol.so/gctl/gctl-v0.20.0/' ``` Check it against its line of the release's `SHA256SUMS`. On macOS, write `shasum -a 256 -c` in place of `sha256sum -c`. If the check fails, it exits 1; then do not run the next command, and `./gctl` stays as it was: ```sh curl -q -fsSL --proto =https https://dl.groundcontrol.so/gctl/gctl-v0.20.0/SHA256SUMS | grep -x "[0-9a-f]\{64\} " | sha256sum -c ``` When the check printed `: OK`, write the asset as `./gctl` in the current directory and make it executable: ```sh mv '' gctl && chmod +x gctl ``` A person can install the same release with the release's installer instead. The gctl README prints it as one command: ```sh sh -c 'script=$(curl -q -fsSL --proto =https "https://dl.groundcontrol.so/gctl/gctl-v0.20.0/install.sh") || exit; case $script in "#!/bin/sh"*) printf "%s\n" "$script" | GCTL_INSTALL_SOURCE=public sh ;; *) printf "%s\n" "gctl install: result=install_fetch_failed reason=the response is not the install script, so nothing was run remediation=check that the URL names a gctl-v tag, then rerun the install command" >&2; exit 1 ;; esac' ``` 2. Connect to your user's account. When your user asks how they can connect you, first name the three ways in "Ways to connect" below and what each costs: gctl, one file to install, with no restart and only the output of its commands in your context; the HTTP API, nothing to install, but you build each request from its operation's description; and MCP, a connector your user adds and approves in a browser, often a client restart, and tool descriptions in your context on every turn. The sign-in routes below are how gctl connects. If your user has an account, give them one step, and stop until they have done it: in their own terminal on this machine, as the same computer user you run as, they run this command, sign in on the page it opens in their browser, and click "Approve and connect": ```sh ./gctl --host login ``` If no browser opens, gctl prints what to do instead. gctl then holds a session for your user, which your own gctl commands on this machine use. The session lasts 365 days, and gctl renews it on its own while it is in use: for a session signed in or renewed by gctl v0.11.0 or later, the first gctl command after the session is halfway through its life renews it for another 365 days, so a session that a gctl command uses at least once every 182 days does not end. A session signed in with an earlier gctl renews only in the last minute before it ends, until its first renewal; your user runs `./gctl --host login` once after upgrading to start this schedule. A session left unused for longer can end; gctl then stops with `re-authenticate with gctl login`, and your user runs `./gctl --host login` again. That session is enough to do governed work, so a separate agent key is optional; you can add one later. Governed commands then use the workspace that session signed in to, so they need no `--tenant`; `./gctl --host status` prints its workspace ID as `oauth_tenant_id`. Some gctl commands refuse a signed-in session and need an API key. Never ask your user for their password. If you start the sign-in yourself, do not run `gctl login` or `gctl login --manual`. The browser sign-in needs its command to keep running until your user approves, and `--manual` needs a person to paste into its terminal. Run this command instead. It prints a link (`verification_uri_complete`) and a code (`user_code`), keeps the sign-in, and returns: ```sh ./gctl --host login --device --no-wait ``` Give your user the link and the code, and stop until they tell you that they approved: they open the link in a browser on any device, sign in there, and enter the code when the page asks for it. The link and the code expire at the `expires_at` it printed; after that, start again with the command above. When your user has approved, run this command. It continues the same sign-in and stores the session. If it prints a new link and code instead, the first link and code expired; give your user the new ones: ```sh ./gctl --host login --device ``` If your user owns the workspace and would rather not use a browser, they can mint a key for you instead. They run this in their own terminal, with their password in the file ``, and tell you only ``. Do not run it yourself: ```sh ./gctl --host credential bootstrap --email --password-file --out ``` Before you run `agent originate`, ask your user: "Do you already have a Ground Control account?" Ask even when they said "sign me up". Then stop until they answer; do not assume the answer is no. If they say yes, sign in to their workspace with `login` or `login --device --no-wait` above, and do not create a workspace. If they say no, create one. Agent Origination creates a new, empty Ground Control workspace, an identity for the agent in it, and that identity's first API key. It costs nothing. The workspace holds no one else's data, and the key reaches only this workspace. Agent Origination never reaches a workspace your user already has. `` is the host that served this file. The name and slug are labels. If your user gave none, use the name of the project you are working on, and a slug of lowercase letters, digits and hyphens that no other workspace uses. End the slug with a hyphen and five random lowercase letters and digits, as in `my-project-`, where `` is five lowercase letters or digits you pick fresh each time, not a fixed suffix, so that it is unused the first time; gctl refuses a taken slug and says what to do. The key is returned only once: gctl writes it to ``, a new file only you can read, and never prints it. Keep the name inside the single quotes; write each single quote in the name as `'\''`, so the name Brian's project is `--name 'Brian'\''s project'`: ```sh ./gctl --host agent originate --name '' --slug --key-file ``` The output prints `tenant_id`, the workspace ID, `api_key_id`, the key's ID, and `renewal_service_principal_id`, the identity the key's renewal names. Nothing renews the key automatically: you renew it by running the `renew_command` that `agent originate` printed before the key expires, as shown below. 3. Make the first governed call. With the Agent Origination key, the command below exits 0 and lists the workspace's worker slots, which are empty in a new workspace. After your user's `gctl login`, run `./gctl --host --auth oauth --tenant initiative list`, which lists the workspace's initiatives. With a key your user minted, run `./gctl --host --api-key-file credential current`, which prints the key's `tenant_id`, then `./gctl --host --api-key-file --tenant status`. `./gctl --help` lists every noun, and `./gctl --help` lists its verbs. A command whose help says it requires consent, such as `library item create`, needs `--yes`; without it, gctl exits 8 and changes nothing. ```sh ./gctl --auth gateway --api-key-file --tenant --host slot list ``` The Agent Origination key lasts 365 days. Before then, the agent renews it itself with the command below, a governed call like any other; it is the `renew_command` that `agent originate` printed. It names the `renewal_service_principal_id` that `agent originate` printed, and `--expires-in 364d` sets the new key's `expires_at` 364 days away, a day inside the 365 days a key may last; gctl adds the `idempotency_key` and `correlation_id`. gctl writes the new key over ``, mode 0600, and never prints it. If the renewal is refused, the command exits non-zero and `` still holds the old key, which keeps working: ```sh ./gctl --host --tenant --auth gateway --api-key-file credential create --yes --body '{"service_principal_id":""}' --expires-in 364d --key-file --rotate ``` From then on, `` holds the new key; keep passing it as `--api-key-file`. The command prints the new key's `api_key_id` and `expires_at`. gctl then revokes the old key, with the old key itself, and prints `replaced_api_key_id: (revoked)`; with `--json`, `replaced_key_revoked` is `true`. If gctl cannot revoke it, the renewal still landed: `` holds the new key, which works, and gctl exits 9 and says so; do not run the renewal again. Keys minted from it for subagents move to the new key and keep working; each still expires at its own `expires_at`. Control comes from three places. The workspace is isolated from every other tenant. Governed changes are recorded with the identity that made them, so the user can see what the agent did. Governance policy (allow, deny or require review) decides the agent's governed calls. The agent invites its user to the workspace as an owner (`gctl tenant invite --email --role owner --yes`) only when the user asks, or says yes when offered, and uses the email the user gave. Unless your user named the role, tell them in one line that you will invite `` as owner, with full control of the workspace, and run it when they say yes. In a workspace your user owns, they invite people from the web app; `gctl tenant invite` takes only the Agent Origination key (over HTTP, the Agent's durable OAuth token with the default bridge scopes also works). An unrenewed key stops working after 365 days. The empty workspace remains; closing it needs a signed-in workspace owner. The agent ends its own access by revoking its key with the command below, with the `api_key_id` that `agent originate` printed; gctl adds the `idempotency_key` and `correlation_id`. After a renewal, it revokes the renewed key the same way, with the key in `` and the `api_key_id` the renewal printed: ```sh ./gctl --host --tenant --auth gateway --api-key-file credential revoke --yes --body '{"reason":"agent finished"}' ``` 4. From an empty workspace to a finished run. Run these commands in this order, one per step: they create an initiative and a scope node in its work breakdown, open a work package and place it on that node, prepare a workflow and an Agent, launch the run, work it yourself, and close the run and the package. `package open` takes IDs you choose: `` is one new UUID, sent as both `package_id` and `work_package_id`, and ``, `` and `` are new UUIDs too. `wbs place-package` makes the scope node the package node that holds the package, which links the package to the initiative. If placement fails, the package already exists: its ID is `work_package_id` in the `package open` output. Rerun `wbs place-package` with that ID; do not run `package open` again. Before each noun, pass the global flags of your step 3 call: `--host --tenant `, then `--auth gateway --api-key-file ` for a key, or `--auth oauth` for your user's `gctl login`. ``, `` and `` are UUIDs you choose. `./gctl --help` lists a noun's verbs, and `./gctl --help` describes the verb and its body: ```sh ./gctl initiative create --title '' --objective '<objective>' --scope '<scope>' --yes ./gctl wbs add '<initiative>' --yes --body '{"title":"<title>","description":"<description>","node_kind":"scope"}' ./gctl package open --yes --body '{"command_id":"<new-uuid>","package_version":"1","package_id":"<package>","work_package_id":"<package>","initial_revision_id":"<package-revision>","package_type_id":"core.initiative_candidate","creation_source":"operator_command","lineage":{"source_external_binding_refs":[]},"supplied_scope_tree":{"scope_tree_id":"<new-uuid>","scope_tree_revision_id":"<scope-tree-revision>","nodes":[{"scope_node_id":"<scope-node>","title":"<title>","summary":"<description>","node_kind":"leaf_package","leaf":true,"display_order":0}]},"initial_revision":{"package_version":"1","title":"<title>","package_type_revision_ref":"package-type://core.initiative_candidate/v1","leaf_scope":{"scope_tree_revision_id":"<scope-tree-revision>","scope_node_id":"<scope-node>"},"risk_class":"medium","control_intensity":"standard","acceptance_criteria":["<check>"],"closeout_criteria":["<check>"],"consumed_baseline_refs":[],"governed_context_revision_id":"<new-uuid>","requirement_set_revision_id":"<name>","dependency_graph_revision_id":"<name>","artifact_refs":[],"external_binding_refs":[],"subject_hash":"<name>","rendered_context_hash":"<name>"}}' ./gctl wbs place-package '<initiative>' '<wbs-node>' --yes --body '{"expected_revision":1,"work_package_id":"<package>"}' ./gctl package revision submit '<package>' '<package-revision>' --yes --body '{"command_id":"<new-uuid>","package_version":"1","work_package_id":"<package>"}' ./gctl package revision promote '<package>' '<package-revision>' --yes --body '{"command_id":"<new-uuid>","package_version":"1","work_package_id":"<package>","promotion_rationale":"<why>"}' ./gctl draft update '<draft>' --yes --body "$(cat workflow-draft.json)" ./gctl draft package-type-binding set '<draft>' --yes --body '{"base_revision":1,"package_type_key":"core.initiative_candidate"}' ./gctl agent create --display-name '<agent-name>' --yes --body '{"classification_data_class":"customer_private_enterprise_owned","retention_class":"large_artifact"}' ./gctl agent revision ingest '<agent>' --yes --body '{"expected_base_revision_id":"<agent-base-revision>","expected_head_generation":1,"expected_current_generation":1,"content":"<agent-instructions>","classification_data_class":"customer_private_enterprise_owned","set_current":true}' ./gctl binding workflow create --yes --body '{"workflow_id":"<workflow>","display_name":"<workflow-name>"}' ./gctl binding node create '<workflow>' --yes --body '{"node_key":"primary","execution_kind":"respond_once","expected_draft_generation":1,"local_instructions":["<node-specific-instruction>"]}' ./gctl binding node bind '<workflow>' '<binding-node>' --yes --body '{"agent_id":"<agent>","track_current":true,"expected_draft_generation":2}' ./gctl binding workflow accept '<workflow>' --yes --body '{"expected_draft_generation":3}' ./gctl draft compile '<draft>' --yes --body '{"draft_revision":2}' ./gctl workflow deploy '<workflow>' --yes --body '{"draft_id":"<draft>","draft_revision":2}' ./gctl workflow publish '<workflow>' --yes --body '{"draft_id":"<draft>","draft_revision":2}' ./gctl package launch '<package>' --yes --workflow-definition-id '<workflow-definition>' --workflow-definition-revision-id '<workflow-definition-revision>' --expected-work-package-revision-id '<package-revision>' --body '{"self_provisioned":true}' ./gctl slot list --execution-run-id '<run>' --limit 10 ./gctl slot claim '<run>' '<slot>' --yes --body '{"assigned_state_key":"primary","mode_key":"active","worker_kind":"agent","source_surface":"gctl","expected_attempt_index":1,"client_instance_id":"<name>","process_generation":1,"process_nonce":"<name>","capability_summary":["<capability>"]}' ./gctl worker progress '<worker-instance>' --yes --body '{"lease_id":"<lease>","lease_epoch":1,"progress_summary":"<summary>"}' ./gctl worker checkpoint '<worker-instance>' --yes --body '{"lease_id":"<lease>","lease_epoch":1,"checkpoint_kind":"manual_snapshot","durability_class":"checkpointed","recovery_strategy":"resume_session","resume_ready":true,"state_key":"implementation","covered_execution_sequence_start":1,"covered_execution_sequence_end":3,"integrity_hash":"sha256:<64 hex>","state_manifest_refs":["<new-uuid>"]}' ./gctl worker finish '<worker-instance>' --yes --body '{"lease_id":"<lease>","lease_epoch":1,"final_checkpoint_id":"<checkpoint>"}' ./gctl closeout run-request '<run>' --yes --body '{"command_id":"<new-uuid>","correlation_id":"<name>","evidence_refs":[{"ref_type":"checkpoint","ref_id":"<checkpoint>"},{"ref_type":"receipt","ref_id":"<receipt>"}]}' ./gctl --json package get '<package>' ./gctl closeout package-request '<package>' --yes --body '{"command_id":"<new-uuid>","correlation_id":"<name>","evidence_refs":[{"criteria_index":0,"ref_type":"workpackage_open","ref_id":"<closeout-evidence-ref>"}]}' ``` ```text For a client that cannot run gctl: each command's operation_id, in the same order. GET /v1/operations/{operation_id} gives its method, route and body. gctl adds the idempotency_key, command_id and correlation_id that each route requires, except package open, package revision submit and package revision promote, whose --body must carry command_id while gctl adds their idempotency_key and correlation_id; over HTTP, send the body GET /v1/operations/{operation_id} describes, with every field it marks required. initiative create post_v1_project_controls_initiatives wbs add post_v1_project_controls_initiatives_by_initiative_id_wbs_nodes package open post_v1_project_controls_packages wbs place-package post_v1_project_controls_initiatives_by_initiative_id_wbs_nodes_by_wbs_node_id_package_placement package revision submit post_v1_project_controls_packages_by_package_id_revisions_by_revision_id_submit package revision promote post_v1_project_controls_packages_by_package_id_revisions_by_revision_id_promote draft update put_v1_governed_work_workflow_drafts_by_draft_id draft package-type-binding set put_v1_governed_work_workflow_drafts_by_draft_id_package_type_binding agent create post_v1_tenants_by_tenant_id_agents agent revision ingest post_v1_tenants_by_tenant_id_agents_by_agentID_revisions_ingest binding workflow create post_v1_tenants_by_tenant_id_workflow_agent_binding_workflows binding node create post_v1_tenants_by_tenant_id_workflow_agent_binding_workflows_by_workflowID_nodes binding node bind post_v1_tenants_by_tenant_id_workflow_agent_binding_workflows_by_workflowID_nodes_by_nodeID_bind binding workflow accept post_v1_tenants_by_tenant_id_workflow_agent_binding_workflows_by_workflowID_accept draft compile post_v1_governed_work_workflow_drafts_by_draft_id_compile workflow deploy post_v1_project_controls_workflows_by_workflow_id_deployments workflow publish post_v1_project_controls_workflows_by_workflow_id_publish_revision package launch post_v1_governed_execution_packages_by_package_id_launches slot list get_v1_governed_execution_worker_slots slot claim post_v1_governed_execution_runs_by_execution_run_id_slots_by_worker_slot_id_claim worker progress post_v1_governed_execution_worker_instances_by_worker_instance_id_progress worker checkpoint post_v1_governed_execution_worker_instances_by_worker_instance_id_checkpoints worker finish post_v1_governed_execution_worker_instances_by_worker_instance_id_finish closeout run-request post_v1_governed_execution_runs_by_execution_run_id_closeout_requests package get get_v1_governed_work_packages_by_package_id closeout package-request post_v1_governed_execution_packages_by_work_package_id_closeout_requests ``` Use one credential for the whole run: the Agent Origination key, the `role:owner` key your user minted with `credential bootstrap`, or the workspace owner's `gctl login` session. The worker key that `credential bootstrap --worker-key` mints cannot finish it: it is refused at `package revision promote` and `workflow publish`. Every other `<value>` is in an earlier command's `--json` output, under `data`: the `initiative:` and `wbs_node:` entries of `resource_refs`; `resource.agent_id` and `revision.revision_id` from `agent create`; `node_json.node_id` from `binding node create`; `workflow_definition_id` and `workflow_definition_revision_id` from `workflow publish`; `execution_run.execution_run_id` and `worker_slot.worker_slot_id` from `package launch`; for `<capability>`, every string of the slot's `claim_contract.capability_summary` from `slot list`, in the listed order; `worker_instance_id` and `lease.lease_id` from `slot claim`; `checkpoint_id` from `worker checkpoint`; `receipt_id` from `worker finish`; and for `<closeout-evidence-ref>`, `package.current_revision.closeout_evidence_refs[0].ref_id` from `package get`, which is `<package>`: `package open` declares the package's own open, `ref_type` `workpackage_open`, as the evidence for its closeout criterion. The first `draft update` creates the draft, so it sends `title` and `graph` and no `base_revision`. Its `graph` has a trigger, the agent node `primary`, which `slot claim` sends as `assigned_state_key`, and a terminal. Save this as `workflow-draft.json`: ```json {"title":"<title>","graph":{"metadata":{"name":"<name>"}, "nodes":[{"id":"trigger","type":"clutch.trigger","title":"Package ready","data":{},"visualData":{"x":0,"y":0}}, {"id":"primary","type":"clutch.agent","title":"Do the work","data":{},"visualData":{"x":120,"y":0}}, {"id":"terminal","type":"clutch.terminal","title":"Complete","data":{"terminal":{"outcome":"succeeded"}},"visualData":{"x":240,"y":0}}], "connections":[{"outputNodeId":"trigger","outputId":"next","inputNodeId":"primary","inputId":"input"}, {"outputNodeId":"primary","outputId":"next","inputNodeId":"terminal","inputId":"package"}]}} ``` `draft update` prints the draft's `revision`, `1` after the create, which `draft package-type-binding set` sends as `base_revision`. A later `draft update` sends `title`, `graph` and `base_revision` together; then send the `revision` it printed as `draft_revision` in `draft compile`, `workflow deploy` and `workflow publish`. When a step is refused, gctl prints the server's refusal. The refusals this sequence can meet: - `package revision promote` with the worker key: exit 4, `action_not_authorized`, `reason` `missing_scope`, `action` `project_controls.package.promote`. Run it with your user's `role:owner` key or the workspace owner's `gctl login` session. - `workflow publish` with the worker key: exit 4, `policy_denied`, `reason` `missing_scope`, `action` `project_controls.workflow.publish`. Run `draft update` through `package launch` with your user's `role:owner` key: it cannot publish a workflow the worker key deployed (`object_not_found`), and the worker key cannot launch a workflow it published (`package_launch_authority_rejected`, `workflow_start_grant_missing`). - The first `draft update` with a `base_revision`, or a later one without it: exit 6, HTTP 409 `stale_revision`, `details.field` `base_revision`. - `slot claim` with any other `capability_summary`: `worker_capability_mismatch`. - `closeout package-request` that does not cite `closeout_evidence_refs[0]` with `criteria_index` `0`: HTTP 422 `package_closeout_prerequisite_rejected`, `details.reason` `missing_revision_artifact_evidence`, `details.criteria_missing` `[0]`. `closeout run-request` answers `"status":"applied"` and `"applied_run_status":"completed"`: the run is finished. `closeout package-request` answers `"status":"applied"` and `"work_package_status":"completed"`: the package is finished. ## Ways to connect - **gctl** (recommended for an agent with a shell). The agent installs one file and calls governed operations from the shell, with no client configuration and no restart. Only the output of the commands it runs enters its context. - **The HTTP API**, for a client that cannot run gctl, such as one with no shell or one that cannot run a downloaded binary; an agent that has a shell but whose gctl install was blocked asks its user, as step 1 says, and does not use this way. The agent sends JSON to `<api>`, the `clutch_api_base_url` field of `https://<host>/.well-known/oauth-authorization-server` (`https://api.groundcontrol.so` on groundcontrol.so); `GET /v1/operations/search?q=<goal>` and `GET /v1/operations/{operation_id}` describe each operation. The full OpenAPI document, `GET <api>/v1/openapi.json`, needs a key with `published_contract.read`. - **MCP**, for a host with no shell, such as a chat app. Claude's settings call an MCP server a "connector". The user adds `https://<host>/v1/mcp?profile=compact` as a custom connector, approves it in a browser, and often restarts the client. The client must speak MCP protocol version `2026-07-28`. The agent gets Ground Control tools whose descriptions use context on every turn. ## Agent Onramp - [MCP resource discovery](/.well-known/oauth-protected-resource) - [MCP bridge documentation](/docs/mcp-bridge) - [Agent Onramp entry](/docs/mcp-bridge#agent-onramp) This entry works with ordinary HTTPS requests. A browser, JavaScript, MCP OAuth and an existing tenant are optional. - New connections use the canonical compact MCP URL: `/v1/mcp?profile=compact`. - `{api_origin}` is the `clutch_api_base_url` field of `/.well-known/oauth-authorization-server`. An authenticated `clutch_start` response also returns it as `api_origin`. Send HTTP writes to that origin directly. - Fetch the unauthenticated machine-readable start descriptor with `GET {api_origin}/v1/agent-origination`. It publishes the `POST` URL, required properties, `Idempotency-Key`, one-time credential rule, next API origin, renewal route, and `next_connection.renewal`, which says how the agent renews the root key (nothing renews it automatically). - Use bounded operation discovery: `GET /v1/operations?limit=50`, `GET /v1/operations/search?q=<goal>`, and `GET /v1/operations/{operation_id}`. - `POST {api_origin}/v1/agent-origination` with `name`, `slug`, and a stable `Idempotency-Key` creates a tenant, a nonhuman root principal for the agent, and its first machine credential, before tenant-bound login. The secret is returned only once. The root key lives up to 365 days (`credential.expires_at`). Nothing renews it automatically: the agent renews it before then, with no human and no new sign-in. The answer's `connection.renewal` names the route, the root's own `service_principal_id`, and `renewed_by: "self"` (the root key presents itself). Send that `service_principal_id` with a future `expires_at` at most 365 days away, `idempotency_key`, and `correlation_id` to `POST /v1/governed-execution/machine-credentials`. The new secret is returned once, and the old key stays valid until it expires or is revoked. A renewal through a key that is revoked, or expired by the database clock, when the renewal rechecks it under the tenant lock (after any wait for that lock) mints nothing and is refused with 401 `auth_denied`. - The originated root credential is scoped to its own new tenant. It holds `role:owner`, the workspace owner's authority, while its root Agent and origination binding are live, plus the enumerated baseline scopes `mcp:bridge.read`, `mcp:bridge.call`, `worker_facade.claim`, `governed_execution.read`, `governed_execution.write`, `tenant_agent.read`, `tenant_agent.write` and `saas.machine_credential.manage`. A workspace originated before the owner role keeps its enumerated root bundle. Tenant governance still decides each governed action the role holds: `deny` refuses it, `require_review` holds it for a reviewer, and `allow` or no binding lets it run. The root credential may tighten an authoring governance binding with `PUT /v1/project-controls/review-authority/authoring-governance-bindings/{governance_key}`, but loosening one needs `approval.decide` and is refused to it. It cannot release a hold, decide an approval or write a review-authority contract; a human does those. Without a human, it can create an initiative, create a work package with `packages:create-active`, create and read library items, instantiate a workflow template and set a draft's package type binding, promote a package revision, publish a workflow with `publish-revision`, and launch and claim work. The workspace's package promotion policy and workflow publish policy still decide each promote and publish. The initiative, library and workflow-draft writes take the workspace's MCP-authoring decisions (allow, deny or require_review), as the dedicated MCP tools do. An agent cannot widen a workspace that has no human owner. When a key there lacks a scope, the refusal returns `recovery_action` `originated_workspace_path`. Its `clearing_condition` names the agent's path, which is to use the root credential, to mint a key from it (a minted key holds only the root's enumerated scopes), or says that no key in the workspace can hold the scope. It never asks for an owner password. - Agent Origination allows 20 new tenants per requester per minute. A refused request returns HTTP 429 with a `Retry-After` header; same-key retries do not use this limit. - An existing authorized connection can use the compact URL, call `clutch_start`, then search or browse, describe, and invoke an operation. An HTTP-capable host can use the exact `http_call` recipe from `clutch_describe_operation`. - A supervisor that can run gctl gives each subagent a key of its own, so the subagent never holds the supervisor's key and no one signs in. With the Agent Origination key in `<key-path>`, it first creates the subagent's principal; the output prints its `service_principal_id`: ```sh ./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> agent child create --name '<subagent-name>' ``` - It then mints the subagent's key into `<subagent-key-path>`, a new file with mode 0600; gctl never prints the key. `--expires-in 364d` sets the key's `expires_at` 364 days away, a day inside the server's 365-day maximum, which leaves a day for a clock that runs ahead of the server's. The output prints the key's `api_key_id`, its `expires_at` and the command that renews it. The subagent passes `<subagent-key-path>` as `--api-key-file`: ```sh ./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> credential create --yes --body '{"service_principal_id":"<service_principal_id>"}' --expires-in 364d --key-file <subagent-key-path> ``` - The supervisor mints the subagent a new key before it expires, with no human: the same command with `--rotate` writes the new key over `<subagent-key-path>`, so the subagent keeps passing the same path. gctl then revokes the old key, with that key, and prints `replaced_api_key_id: <api_key_id> (revoked)`; with `--json`, `replaced_key_revoked` is `true`: ```sh ./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> credential create --yes --body '{"service_principal_id":"<service_principal_id>"}' --expires-in 364d --key-file <subagent-key-path> --rotate ``` - When the subagent is done, the supervisor revokes each key it minted for it, with the `api_key_id` that the mint or the renewal printed. A revoked key is refused with HTTP 401 at its next call: ```sh ./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> credential revoke <api_key_id> --yes --body '{"reason":"subagent finished"}' ``` - An MCP-only child can receive a machine-provisioned delegated connection from an HTTP-capable supervisor holding the tenant's originated-root machine credential, without a browser session. - A supervisor that cannot run gctl creates the child principal with `POST /v1/agent-origination/child-principals`, sending `name` and `idempotency_key`, then sends the child's `service_principal_id`, a future `expires_at`, `idempotency_key`, and `correlation_id` to `POST /v1/governed-execution/machine-credentials`. The server sets no default `expires_at`, and the latest it accepts is 365 days from the request. - Omit `scopes` to keep the delegated credential bounded by the parent's live scopes, or pass an exact subset of those scopes. With `scopes` omitted, a child copies the root's non-role scopes, so it holds the baseline scopes without `role:owner`; a role item is never copied. Pass an exact subset to keep the child's list narrower; a child that names `role:owner` is refused as malformed. - The child credential secret is returned only once. The child connects with it to `/v1/mcp?profile=compact`. - A child stays bound to the parent key that minted it. When the originated root renews its own key, every live child of the presenting key moves to the new key in the same request, so the children keep working and need no new secret. A child minted under the old key after the renewal stays on the old key, and revoking the old key stops that child; revoking the new key stops the moved children. Each child still expires at its own `expires_at`: mint the child a new key before then. The supervisor can revoke its own descendants through `POST /v1/governed-execution/machine-credentials/{api_key_id}/revoke`. - A host that can neither run gctl nor make HTTPS requests connects through MCP: its user adds the connector and approves it in a browser, or a supervisor provisions a child credential for it. **Hand the workspace to a human.** Do this only when your user asks, or says yes when you offer it. Hand over the workspace where you did the work; if you created a second workspace, ask your user which one they mean. Unless your user named the role, tell them in one line that you will invite `<email>` as owner, with full control of the workspace, and run the invite when they say yes. "Add me so I can take over" does not name the role, so tell them then too. The root credential runs `./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> tenant invite --email '<email>' --role owner --yes` with the email your user gave. In a workspace your user owns, they invite people from the web app; `gctl tenant invite` takes only the Agent Origination key (over HTTP, the Agent's durable OAuth token with the default bridge scopes also works). Without gctl, it calls `POST /v1/workspace-admin/invites` with that `email` and `role` `owner`. The answer has no token; its `human_next_step` names the person's one step: open the link in the invitation email, then enter the existing account's password or choose a new one and confirm the address. Read the handover back with `./gctl --host <host> --tenant <tenant_id> --auth gateway --api-key-file <key-path> tenant members`, or `GET /v1/workspace-admin/members` with the same credential: the invite is listed as pending until the person accepts it, it is cancelled, or it expires at the invite answer's `expires_at`; once the person accepts, they are a member with `role` `owner`. The root credential keeps working after a human owner joins. The compact profile is a short discovery surface, not a worker-only product. The authenticated credential can use the full platform operation surface that its tenant authority permits. The platform keeps tenant boundaries and tenant policy authoritative. The only human-only exception is performing a human approval, such as a package-review decision by the server-admitted human reviewer. It is not a Web or MCP approval step. MCP permit minting needs no browser session: an agent mints a permit under its own identity when the contract assigns it the effect and the request's quorum is satisfied. The explicit `/v1/mcp?profile=read` and `/v1/mcp?profile=full` URLs remain available. The unqualified `/v1/mcp` URL remains the historical full-catalog compatibility choice. ## Existing-tenant machine-credential enrollment Use this enrollment path only when the tenant already exists. A new workspace comes from Agent Origination above, which needs no browser signup. A tenant must already exist; an agent cannot create one through the machine-credential enrollment routes. Current workaround: an owner uses a verified-email browser session with the `saas.api_key.create` permission and authentication that is at most five minutes old to `POST /v1/machine-credentials/enrollment-tokens` with `tenant_id`, then gives the returned one-time `enrollment_token` to the agent. The owner/browser issuance requirement is a known implementation gap. The token's default lifetime is 24 hours; an issuer may choose an expiry no more than 7 days from issuance. The agent then calls `POST /v1/machine-credentials/enrollments` with the existing `tenant_id`, `enrollment_token`, `display_name`, tenant-wide scopes limited to `mcp:bridge.read`, `mcp:bridge.call`, `worker_facade.claim`, `governed_execution.read`, `governed_execution.write`, and the credential `expires_at`. Redemption creates one credential in the existing tenant and consumes the token; it cannot create a tenant or be reused. A workspace owner, admin or member can give an agent a role instead of a scope list. In a browser session that is at most five minutes old, the caller calls `POST /v1/governed-execution/machine-credentials` with the agent's `service_principal_id`, `expires_at`, `idempotency_key`, `correlation_id`, and `scopes` holding one tenant-wide item such as `{"permission": "role:member"}`; an owner or admin may add other scope items, and more than one role item is refused as malformed (`scopes`, `format`). The role must be at or below the caller's own role (owner > admin > approver > member > viewer): an owner may ask for any role, an admin for `role:admin` and below, and a member for `role:member` or `role:viewer`, sent alone. A member gives its own Agent a credential this way: it binds an Agent it owns with `POST /v1/service-principals/agent-bindings/{agent_id}/bind`, which creates a service principal the member owns, then mints the role key on that principal. A member binding or minting for an Agent it does not own is refused. A higher role, a plain scope from a member, or an OAuth token restricted to scopes is refused with `policy_denied` (`scopes`, `outside_authority`). A machine credential cannot name a role item, except the originated root renewing its own key, which may name `role:owner`. With `scopes` omitted, when the agent renews its own key, the renewed key keeps its role, and a child copies only its parent's non-role scopes: a role item is never copied. If the parent holds only role items, nothing is left to copy, and the mint is refused with HTTP 400 `malformed_request` (`scopes`, `format`); pass explicit `scopes`. A role key is refused (`service_principal_id`, `wrong_owner`) on a principal the owner did not delegate to it. The key's role stays capped by the minting member's current role, so demoting that member narrows the key, and tenant governance (`deny`, `require_review`) still applies to it. It is listed, expires and is revoked like any other key. Nothing renews the redeemed key automatically. The agent renews it before it expires by running the renewal itself: it calls `POST /v1/governed-execution/machine-credentials` with the redeemed key and names its own `service_principal_id`, with `expires_at`, `idempotency_key`, `correlation_id`, and optional `scopes`. Omitted or empty `scopes` give the successor the key's own scopes. A requested scope must be one the key holds; a wider scope is refused with `policy_denied` (`scopes`, `outside_authority`), and a scope that names `object_type` or `object_id` is refused as malformed. `expires_at` may be no more than 365 days from now. The successor secret is returned once. The successor is owned by the same owner membership in the same tenant, the owner can revoke it, and the presenting key stays valid until it expires or is revoked. Tenant governance decides the renewal: a `deny` binding refuses it, and under `require_review` it is held until a human reviewer approves it, then the key resends the same request with `held_attempt_id` to collect the secret. This key has no owning Agent, so `POST /v1/governed-execution/machine-credentials/recover` answers 403 `policy_denied` with reason `no_owning_agent`, and `POST /v1/machine-credentials/enrollment-tokens` answers 403 `actor_not_authorized`; each refusal's `required_action` names the renewal.