API & MCP setup
Native Hub dashboard and board_state reads use the same filtered board
inventory and bounded operational observations as the Work page. Their native
results contain per-project workflow definitions, lane totals, paged items and
operational items, plus running, queued_inventory, open,
closed_inventory and total counts. Queued inventory includes held items in
dispatchable lanes without a live worker; it is not a dispatch-readiness count.
Terminal states do not contribute to running or queued inventory. board_state accepts a
state filter. Both reads accept an optional project and an item limit (default
100, maximum 200); omitted projects resolve through current readable grants.
The item budget is shared across returned projects. truncated marks omitted
projects or items, so aggregate counts may cover only returned projects. Use
project-scoped reads or work_list cursors to continue inventory reads.
aggregate_dispatch_readiness remains explicitly unavailable; use the advertised
eligibility_tool (explain_item) for current native admission evidence,
including human ownership, dependencies and registered-runner conditions.
Native Hub operators can call stop_run directly with project_id, request_id,
identifier, expected_revision (a decimal string), lease_id, fencing_token
(an integer), and destination (Blocked, Backlog, Cancelled, or Todo).
Use the current item revision and exact active lease/fence from the supported
work-item and attempt reads. Optional priority uses ranks 1–4; reason accepts
up to 280 bytes. The existing stop owner checks current write access,
revision, workflow transition and lease fence atomically, preserves retained
source, and releases only that attempt. The returned action_id is readable
through action_result; its result records the released lease/fence and the
item's resulting state and revision. Reuse the same request_id and arguments
for a retry. This operation requires no conversation or coordinator turn and
does not grant access to private conversations.
On an enrolled runner host, discover the local_projects toolset on the
runner's authenticated /mcp connection. local_project_configuration reads the actual
local revision and redacted selected/effective policy. Its admin commands apply
an approved policy, drain current work or detach one settled migrated
project. They use the existing operator command API and authority and receipt owner;
see managed local project configuration
for exact revision, policy and handoff requirements. Cloud also reaches the
enrolled runner's configuration owner through its existing heartbeat transport.
Select runner_id when more than one granted
runner reports the project. Commands through Cloud additionally require that
runner's current expected_runner_revision. Runner administration and local
project commands require an Admin key. Hosted owners and admins have organization
runner access; members require runner management grants on the organization’s
projects. An all-project Write key
does not grant runner administration. A stopped or missing runner owner
returns an explicit constraint; these calls never start or enable a board.
Verify registered and runtime_registered through a
fresh read after a saved removal; a command retry returns its original receipt.
Policy approval without the web app
Repository configuration reports do not require approval before dispatch. Trusted workflow definitions from the bound repository's default-branch history apply automatically. Explicit approval is required for administrative configuration adoption, such as applying an external local definition or changing local-binding permission. Configuration identity changes do not revoke active leases or invalidate recovered source.
On a self-hosted Hub without the web app, read get_project_policy and
get_onboarding, then use approve_project_policy with the current
expected_policy_id and the chosen observed_policy_id. The REST equivalent is
GET and PUT on /api/v2/organizations/{organization}/projects/{project}/policy,
with GET on the project's /onboarding for observed candidates. Approval needs
administrator authority and preserves revision checks. Cloud owners and admins
can continue approving in Settings > Integrations.
A difference between the selected and effective configuration remains diagnostic information; it does not by itself block claims, revoke leases or invalidate recovered source. The configuration identity records what an attempt used. Legacy configuration-management operations remain separate from dispatch and still require their documented administrative authority and revisions.
Use the Hub's organization-scoped MCP connection (the projects toolset) with
an Admin key authorized for the project. An enrolled runner's credential reports
configuration; it cannot approve it. No configured project list on the runner
is needed. Use the native project ID, such as prj_example.
-
Call
get_project_policywith{"project_id":"prj_example"}. Itsdatacontains the current approval; copydata.policy.policy_idforexpected_policy_id. If it returnspolicy_mismatchbecause no approval exists, use an empty string for first approval. -
Call
get_onboardingfor the same project. Inspectdata.observed_policiesand the selected descriptor, source revision and reporting runners. Choose itspolicy.policy_idasobserved_policy_id. Conflicting or previously approved reports are not new approval candidates; resolve the configuration conflict rather than approving a stale report. An empty candidate list can mean the trusted report already applied; read the current policy again instead of inventing a candidate. -
Call
approve_project_policyusing the same project and a uniquerequest_id, with the followinginput(replace the example policy IDs with the values just read; use""forexpected_policy_idon first approval):{ "project_id": "prj_example", "request_id": "approve-shared-configuration-1", "input": { "expected_policy_id": "policy_current", "observed_policy_id": "policy_selected" } }Omit
input.policywhen approving by reference. Alternatively, supply the complete, unchangedlocal_project_configuration.selected_policyasinput.policy, preserving every field; do not reconstruct its identity. -
Read
get_project_policyagain to confirm the approved identity and provenance. Approval and runner adoption are separate: uselocal_project_configurationto inspect adoption if applying a local configuration. On a revision conflict, read the current state and review the candidate again before submitting a new request. Exact retries use the samerequest_idand unchanged arguments.
For a bearer-authenticated self-hosted Hub, use the instance administrator
credential for REST approval. GET
/api/v2/organizations/{organization}/projects/{project}/policy returns the
approval directly (policy.policy_id); GET the same project's /onboarding
returns observed_policies directly. PUT to /policy takes the two fields
inside input above as its top-level JSON body, without the MCP envelope.
The Hub resolves the stored descriptor and checks expected_policy_id
atomically. A stale expected identity returns HTTP 409; reread before replacing
it. On hosted Cloud, the web app uses the owner/admin
/onboarding/policy route; the instance-admin /policy PUT is not served there.
The hub policy CLI resolves a configured project from a global project list;
it is not the approval path for enrolled runners. Use the API/MCP procedure
above for a native project ID. The legacy policy-revocation operation returns
HTTP 410; pause or drain the runner, or remove project access, to stop execution.
For an external local definition without trusted repository provenance, the
approved authored Cloud descriptor supplies shared behavior. Private overlays
remain on the host and do not prevent adoption. Read selected_policy as the
preview, then apply its exact approved policy ID and source revision after work
settles. Application preserves host execution settings and the prior pause or
drain state; it does not rewrite definition files or runner routing. The durable
Cloud approval supplies the same shared behavior on subsequent reloads and
restarts. applied: true confirms runtime adoption; saved: false means no local
configuration file was changed. Cloud readback retains the completed receipt,
including refusal constraints, in last_operation after later heartbeats.
Trusted repository definitions retain their repository authority: commit the
intended shared files to the bound repository's default branch so the runner
can report and apply that revision automatically. An authored source_revision
digest is not a Git commit.
For the existing worker.allow_local_binding setting, read the effective
allow_local_binding and the complete local_binding_policy or
restricted_binding_policy descriptor. Approve the selected descriptor through
approve_project_policy using the current approved policy identity, then submit
apply_local_project_policy with the matching allow_local_binding, policy ID,
source revision and configuration revision. The owner changes only that field
in the configured project's private detent.local.yaml, preserving its other
settings. Configuration CAS includes the global configuration and actual project
definition. pending means the selected request awaits heartbeat application; read again
for saved and applied evidence. The project must be paused or draining with
settled work. After draining settles, application uses the existing pause owner
before changing policy. The grant
defaults to false and includes connections to other localhost services as well
as temporary test servers; the sandbox tier and host-service grants retain
their existing owners.
ChatGPT and Claude custom connectors (Detent Cloud)
Use https://cloud.detent.build/mcp with OAuth. No API key, custom header,
client ID or client secret is needed. Detent uses dynamic client registration
and the authorization code flow with S256 PKCE. Your normal Cloud sign-in,
workspace membership, role, project grants and organization key policy apply.
A reporter can file issues and use the same tools as with a personal key.
Registration returns a sealed client ID without storing an unused client.
Pending authorization lives in one encrypted browser cookie, expires after ten
minutes, and creates no server rows. Registration bodies are limited to 64 KiB,
with at most 20 redirect URIs and a 2,048-character client ID; the pending
cookie is limited to 3,800 characters. Only authenticated consent stores the
client, grant and single-use authorization code. Anonymous registration and
pending requests therefore need no database quota or cleanup sweep.
Registration and authorization share the existing API token-bucket limiter:
120 requests per minute per client IP with a burst of 30. Excess requests get
HTTP 429 with Retry-After: 60; tokens refill over time. Only the loopback TLS
proxy is trusted to append X-Forwarded-For; client-supplied prefixes cannot
change the caller's quota. This uses the same limiter implementation as API
key authentication, without adding a separate throttling or cleanup mechanism.
ChatGPT
- Open ChatGPT Plugins, select the plus button, then Add custom MCP server. In older interfaces, enable developer mode and use Settings → Apps (or Connectors). Your plan and workspace policy must permit custom servers.
- Enter a name and
https://cloud.detent.build/mcp, and select OAuth and dynamic client registration if prompted. Leave client credentials empty. - Connect, sign in to Detent with your usual WorkOS login, choose the workspace to grant, review the client and return address, and select Connect assistant.
- In a conversation, enable the connector, discover its tools, select your organization and project, and ask it to file an issue. Confirm the issue is attributed to your account.
See OpenAI's connection steps
and MCP authentication guide.
The published callbacks are
https://chatgpt.com/connector_platform_oauth_redirect for issuer-aware servers
and https://chatgpt.com/connector/oauth/{callback_id} for callback-specific
connections. Detent returns the issuer on authorization responses.
Claude (claude.ai or Claude Desktop)
- Open Customize → Connectors → Add → Add custom connector (Organization
settings → Connectors for workspace owners). Enter a name and
https://cloud.detent.build/mcpas the remote MCP URL, then continue. - Choose Sign in now for authentication and Register automatically for the OAuth client when those choices are shown, then add the connector. No client ID or secret is required.
- Sign in to Detent, choose the workspace, review the client and return address, and select Connect assistant.
- Enable the connector in a conversation, discover its tools and select your organization and project before filing an issue. Confirm your account is recorded as the author.
See Claude's custom connector setup
and authentication requirements.
Claude's hosted connector callback is https://claude.ai/api/mcp/auth_callback,
including connections from Claude Desktop.
Grant lifetime and disconnecting
An access token lasts one hour. Refresh tokens rotate on each use and expire
30 days after issuance. Reusing a consumed refresh token revokes the grant.
Rotation replaces the grant's single stored refresh credential. Sealed token
identity and expiry preserve replay detection without retaining token history;
expired tokens are refused without revoking a newer valid credential.
Revoking a grant makes its next MCP request return 401, including requests
using an existing Mcp-Session-Id.
Signed-in owners can GET /api/cloud/account/oauth-grants and
DELETE /api/cloud/account/oauth-grants/GRANT_ID (same-origin browser request).
Personal-key and OAuth MCP clients can use list_oauth_grants and
revoke_oauth_grant to list or revoke their own grants (revocation requires
write scope). Listings include the client, workspace, connection time, last use
and revocation time, never tokens.
Pass a returned next_cursor as cursor to retrieve subsequent pages.
Organizations that block personal keys or require administrator approval also
refuse new assistant grants; ask an organization administrator about its policy.
Bearer API keys remain supported for Claude Code and Codex, including their existing expiry, rotation, revocation and access restrictions.
Account and organization service keys
Personal keys are user-owned and stored in the shared entry authentication store.
The account API creates and lists them at /api/cloud/account/api-keys and
revokes them with DELETE /api/cloud/account/api-keys/:key. These management
routes require the signed-in account's browser session and same-origin writes.
Open Account settings → API keys (/settings/api-keys) to create, rotate
or revoke your personal keys, inspect effective reach today and last use, and
copy secret-free MCP or direct API setup prompts. A key never grants more than
its owner has. GET /api/cloud/account/key-context supplies the current
organizations, roles, permitted projects and single multi-org MCP endpoint.
The key list includes current effective reach and last use. Rotation uses
POST /api/cloud/account/api-keys/:key/rotate: it replaces the token immediately,
retains the key's scope, expiry, approvals and org blocks, and returns the new
secret once. Update clients after saving it privately.
Creation accepts name, permission (read, write, admin),
access_context (global, selected, project), organizations, and either
expires_days (1–90) or never_expires: true. Each organization entry contains
organization_id, project_access (all, selected, project) and
project_ids. Selected project access requires at least one ID; project access
requires exactly one. A global personal key reaches all current and future
organizations, with all projects by default; explicit organization entries can
narrow individual organizations. Selected context reaches only the supplied
organizations. Project context requires exactly one organization and one project.
The secret is returned only on creation or rotation.
Use one bearer key with https://cloud.detent.build/mcp. Project and organization
tools accept organization_id alongside their existing project selectors.
organization_list lists reachable organizations; discovery returns their tool
union. Calls recheck the target organization's current role, grants and plan.
For REST, GET /api/v2/organizations lists reachable organizations; select the
target with X-Detent-Organization for /api/v2/projects and
/api/v2/projects/:project/.... Explicit /api/v2/organizations/:organization/...
and existing per-org MCP endpoints also accept a key that reaches that org.
Browser-only settings routes continue to require browser sessions.
Personal keys act as their human owner. Saving an issue body through REST,
MCP edit_item, or a CLI using a personal key confirms its issue-contract
sections. Collaboration actors record kind: human, the owner's principal,
and via_key; audit records retain the owner and key ID. Service, runner and
worker credentials cannot confirm issue contracts. Revoking a key refuses
subsequent calls without removing earlier human confirmations.
Historical cards with unconfirmed sections require an authorized human to save the current body through the web UI or a personal key. An unchanged body save confirms those sections. Key migration does not backfill confirmations; service, runner and worker keys cannot perform this confirmation.
Org owners/admins manage service keys at
/api/cloud/organizations/:organization/service-keys, using the same creation
fields with one organization and selected or project context. Service keys are
org-owned and remain valid after their creator leaves; permission and project
context still restrict each call.
Open Org settings → API & MCP to manage service keys, member keys, personal key policy and the approval queue. Non-admins see only a link to their own account keys; the management APIs enforce current owner/admin authority.
Org owners/admins read or set personal_keys (allowed, approval, blocked)
through /api/cloud/organizations/:organization/key-policy (GET/PUT). The
default is allowed. approval refuses personal keys until an admin approves
each key with PUT external-keys/:key/approve; blocked refuses all personal
keys, including keys scoped to one project. The old allow_external_keys write
field remains accepted, mapping true to allowed and false to blocked.
Existing false policies migrate to blocked.
GET /api/cloud/organizations/:organization/external-keys includes active
personal keys whose owner still belongs to the org and whose scope includes
it, including unused keys. It returns only this org's reach and last use.
Pending entries form the approval queue. PUT external-keys/:key/block with
{} revokes access to this org without affecting other orgs;
{"project_id":"..."} blocks a single project. An approval never clears an org
block. The owner's account receives a notice through
/api/cloud/account/key-notifications. Calls and policy, approval and block
changes are audited in the target org with key and owner identity.
Personal keys intersect their scope with the owner's current provider and local
membership, role, project grants and entitlement on each call. Membership loss,
key expiry, revocation and org blocking take effect on the next call. Named
refusals include key_organization_denied, key_project_denied,
membership_denied, key_permission_denied, role_denied,
organization_key_policy_denied, organization_key_blocked and key_inactive.
Existing per-organization keys
Previously issued org keys keep working through their existing API and MCP
endpoints. Shared Cloud uses the account key screen for newly issued personal
keys and the org screen for service keys. Existing org keys remain available
through GET /api/v2/organizations/:organization/api-keys and can be revoked
through DELETE /api/v2/organizations/:organization/api-keys/:key.
Existing keys created from a member's account settings are personal keys;
runner and worker credentials remain service credentials. Per-organization
key creation accepts kind: personal (the default for signed-in account
creation) or kind: service. Both retain the existing permission,
organization and project grant checks. Returned key metadata includes kind.
On dedicated deployments, sign in to the intended Cloud organization and open Settings → API & MCP.
Existing /settings/mcp bookmarks open this same page. Create a named key,
choose Read, Write or Admin, and choose an expiry (7, 30 or 90 days, or Never). The creation API accepts 1–90
days
with expires_days, or never_expires: true with expires_days: 0. Keys with no
expiry are listed with expires_at: null.
Project access defaults to All projects, including future projects within
this organization and your current permissions. You can create this key before
any project exists. Choose Selected projects and select at least one project
to narrow access. Existing restricted keys keep their selected grants.
Copy the once-displayed key directly to a private secret store or environment
as DETENT_API_KEY. The page lists scope, all-project access or selected
project names, expiry, fingerprint and revocation state; it never retrieves an existing secret. Revoke keys here.
Removing client configuration does not revoke a key.
The same key authenticates direct API requests and MCP. Effective permissions are the intersection of its scope/project grants and the issuing user's current provider membership, local membership, role and project grants. Removing or replacing membership, losing project access, expiry and revocation deny later calls, including calls on an already initialized MCP session. An existing per-org API key cannot change organizations or impersonate another user. Runner and worker credentials remain separate and cannot connect as operators.
The browser creation API accepts project_access: "all" | "selected" and
project_ids. Omitting the mode and project selection defaults to all projects;
legacy requests supplying project IDs remain selected. Explicit selected mode
without IDs returns 422 with a clear error. All mode with selected IDs is rejected
as ambiguous. Creation and listing metadata both return project_access and
project_ids (an empty array for all-project keys). All mode is durable, never a
snapshot of existing IDs; new authorized projects work with the original key and
MCP session without synchronization or reconnection. Removing grants from a
selected key never changes its mode or widens its access.
The hosted MCP credential_create tool uses the same project_access and
project_ids contract through the shared key owner. Its existing scope, expiry,
current authority and connection-bound secret delivery requirements still apply.
Key management requires the issuing user's browser session and CSRF state.
API and MCP client connections require only Authorization: Bearer, without
browser cookies, sessions or CSRF. The server executes calls directly after checking key scope, project access and
current grants. The MCP client owns user confirmation through its local settings;
there is no server confirmation mode, pending approval or approval URL. Revoked
or expired keys fail every later call and retry. Use narrower keys to limit clients.
Organization URLs
Issue and comment attachment storage uses the authenticated organization and project routes described in Cloud attachments.
Copy the URLs shown on this page. On shared Cloud their complete shapes are:
https://app.detent.cloud/organizations/ORG/mcp
https://app.detent.cloud/organizations/ORG/api/v2/organizations/ORG
The second URL is the direct API base. A dedicated/self-hosted organization uses
its own public HTTPS origin and deployment mount. Keep the complete shared
/organizations/ORG mount. Do not use another organization's endpoint, a project
page, /sse, or an invented /api/v1/state or OpenAPI route. Endpoint discovery
and request payloads are documented in Hub API, especially Native
collaboration and Changes. Operator capabilities documents the real catalog and
deployment availability.
Platform credit administration
Platform administrators use the shared entry's /api/cloud/platform/mcp
endpoint for platform_adjust_ai_credits. Authenticate each request with the
existing entitlement-administrator service bearer token, or with an account
session authorized by the platform entitlement-administrator check and its
X-CSRF-Token. Organization API keys and organization-owner sessions cannot
grant credits. The platform endpoint uses the same MCP protocol as the
organization endpoints and advertises only the platform credit command.
The tool accepts organization_id, idempotency_key, amount_usd and reason.
For example:
{
"organization_id": "org_example",
"idempotency_key": "complimentary-pilot-1",
"amount_usd": "10.00",
"reason": "Complimentary pilot credits"
}
Use a signed decimal string for amount_usd: positive to grant credits,
negative to correct the balance, with at most six fractional digits. Preserve
the key, amount and reason when retrying. The result contains the organization's
current balance in USD micros. The tenant writes the ledger and balance on its
open database; the command requires no tenant restart or database file access.
See Platform AI credit adjustments
for the REST equivalent, bounds and audit contract.
Direct API agent handoff
Use Copy API setup prompt on the same page. It fills in the current organization/base URL and an initial project context. Choose a project actually granted to the key; if the suggested project is not selected on that key, replace it with a granted ID from the key metadata.
The following is the handoff's executable read portion. Populate private
DETENT_API_KEY through the client's secret store, without pasting it into a
conversation, committed file, screenshot, URL, diagnostic output or shell trace.
Replace the public organization/project placeholders before running:
set +x
DETENT_API_BASE='https://app.detent.cloud/organizations/ORG/api/v2/organizations/ORG'
DETENT_PROJECT_ID='YOUR_GRANTED_PROJECT_ID'
curl --fail --silent --show-error --header "Authorization: Bearer $DETENT_API_KEY" "$DETENT_API_BASE/projects/$DETENT_PROJECT_ID"
curl --fail --silent --show-error --header "Authorization: Bearer $DETENT_API_KEY" "$DETENT_API_BASE/projects/$DETENT_PROJECT_ID/work-items?limit=20"
Verify returned project/organization IDs before reporting success. An empty work-item list is valid after the project read verifies the context. Direct API use needs no MCP client. Ordinary writes need Write scope and current write access; consult each operation's documented payload, revision and idempotency requirements. Sensitive operations execute directly when authorized. Configure confirmation in the client and choose the key scope and project access accordingly. Workflow requests still use the application's orchestrator lane owner.
A concise agent handoff is:
Connect directly to the copied Detent API base for this organization. Have me create/select an expiring shared key on Settings → API & MCP with Read scope and the required projects. Ask me to put it directly in your private secret store/environment as DETENT_API_KEY, never this conversation. Send Authorization: Bearer privately with no cookies or CSRF. Run the two documented project/work-item GETs above for a granted project and verify the context. Follow Hub API request schemas and current authorization. Configure sensitive-operation confirmation in the client; authorized calls execute directly. Report success or an access error without secrets.
MCP agent handoff
Hosted allowance refusals are MCP tool results with isError: true, rather than
transport failures. Their JSON content includes code: "allowance_exhausted",
http_status: 429, resource, allowance, consumption, a safe message and
recovery guidance. Current MCP versions also return this as structuredContent.
Inspect billing_usage.hosted_plan_page with owner/admin authority or
billing_usage.billing_export with owner authority and resolve the affected
allowance before submitting another business action. A refusal does not report a
successful write. policy_mismatch directs callers to get_project_policy to
refresh approved policy inputs; uncertain mutation outcomes still require resource
inspection before retrying. Unexpected failures remain redacted as
service_unavailable with a correlation ID.
Native verify_fix answers a Done issue whose verification.state is pending: result: verified records the verifier, and result: not_fixed requires comment, which may embed upload_attachment references, and reopens the issue to Todo. Only the filer, or a member and above, may answer. work_list created_by=me verification=pending lists the caller's issues awaiting verification.
Native file_issue accepts type: bug or type: change_request. Reporters must provide one; other roles may omit it. edit_item changes the type for members, and work_item and work_list return it when set. work_list type=change_request filters by the stored classification independently of labels.
Native file_issue may return possible_duplicates and a challenge_token
without creating an issue. Review the candidates and use supported reads such
as work_item and work_comments when needed to decide whether they concern
the same problem. If filing is already authorized and the evidence clearly
establishes a distinct problem, repeat file_issue without another human
question, keeping the same request_id and content and adding the returned
challenge_token and not_duplicate_of with the challenged issue numbers.
For an actual duplicate, use the existing issue and comment within the caller's
authorization. Ask the human only for genuinely ambiguous overlap, a product
decision, or an action beyond authorization; do not automatically dismiss
candidates. This does not grant filing or native issue-contract confirmation
authority. See the native filing challenge contract
for expiry, replay and related-history behavior.
edit_item replaces the body with the complete reviewed text; it never truncates
or merges partial content. Its body limit is 64 KiB of UTF-8, and the complete
serialized JSON arguments, including escaping and other fields, must fit 64 KiB.
An existing body above 32 KiB can be corrected when the full replacement fits
that request bound. For larger native replacements, use the supported
PATCH /api/v2/organizations/{organization_id}/projects/{project_id}/work-items/{work_item_id}
with idempotency_key, expected_revision and the complete body, under the
same write authority and project grants. That application command accepts a
body up to 256 KiB and a request up to 1 MiB. Personal keys confirm the supplied
body's issue-contract sections; service and worker keys preserve existing
confirmations without confirming changed sections. Comment and filing MCP
limits remain 32 KiB. See native collaboration
for the application request contract.
Native file_issue also accepts an optional report_context object with url (at most 2048 bytes) and app_build, browser, os, viewport and captured_at (at most 256 bytes each), all single-line strings. Unknown keys, oversized or non-string values are refused with invalid_request, and the refusal never repeats the submitted values. work_item returns the stored context; list and board reads omit it. A Reporter bug whose report_context carries browser and app_build satisfies the template's Environment section. Upload network HAR exports (.har) with detent attach or the REST attachment upload API, because upload_attachment accepts only about 44 KB; use upload_attachment for small console log exports (.log). Read either back with read_attachment.
For acceptance inspection, publish complete diagnostic evidence as UTF-8
text/plain, including JSON diagnostics, within 32768 bytes per attachment.
Set content_type: text/plain for upload_attachment; workers using
attach_evidence use a .log or .txt filename. Larger upload capacity does
not expand the inspection bound. Split larger documents at UTF-8 boundaries
into ordered parts of at most 32768 bytes each, preserving every original byte.
Record part order, byte ranges, original document or attachment identity and
SHA-256 in the publication, and retain the original references. Preserve source
identities, hashes, commands, output and provenance limitations; summaries cannot
replace required observations, and missing historical evidence is never invented.
Acceptance validators obtain their own review-bound inspection receipts by
reading each complete supported attachment from offset 0. Ordinary implementation
and Rework reads do not issue these receipts; workers must not manufacture them
or edit source solely to obtain them. In acceptance validation, unsupported MIME
such as application/json returns inspection_error with the supported
publication path. Oversized text returns an authenticated, bounded preview with
truncated: true and inspection_error; full-digest verification does not prove
complete inspection. Neither response supplies read_receipt. Validators inspect
all required ordered parts and independently assess source applicability before
claiming complete evidence. Ordinary reads remain bounded base64 transport.
Reporter filings enter Triage and require nonempty sections. Bugs use Steps to reproduce, Expected, Actual, Where (screen or URL), and Environment (browser/OS and app build; use available context). Change requests use Current behavior, Desired behavior, and Why. Missing or empty sections return invalid_request with the missing headings and create no issue. work_config returns reporter_templates, a map from each type to its effective required headings. It also returns issue_contract.required_sections, the effective engineering headings required for executable work. Both come from the same approved project policy, including workflow overrides; callers must not substitute their own default headings. When the approved policy delegates bounded scheduled repair, issue_contract.scheduled_repair also exposes its policy_id, configured repository, workflow, authorized branches, declared priority (0 Urgent, 1 High), generated contract body, and evidence_schema. Native reports must carry the scheduled evidence stamp and pass independent repository CI verification before that contract can be confirmed. The repair template must match the effective required sections exactly. Configured workflows may authorize an exact branch or explicitly authorize the release/v* family; other wildcards are refused. The legacy CI delegation retains develop and the release/v* prefix family. Origin stamps may retain the corresponding GitHub run URL without an attempt suffix; an explicit attempt must match the independently verified evidence. These reads do not confirm issue criteria or grant permission to dispatch. The New issue form pre-fills these headings when a type is selected.
Projects can override either template in WORKFLOW, using the same list syntax as the engineering issue contract:
## Reporter Contract
### bug
- **Steps to reproduce** — A reproducible sequence.
- Expected
- Actual
- Where
- Environment
### change_request
- Current behavior
- Desired behavior
- Why
An omitted type keeps its default template. Triage is exempt from the engineering issue contract; after promotion into the configured admission lane, the existing contract check requires the member-authored engineering sections and uses the existing Blocked human-action recovery when they are missing or unconfirmed. Preserve the Reporter sections as evidence.
For native file_issue with github_issue_url, read get_project_integration
and get_native_project for the selected project. The URL must match its
attached repository or associated runner checkout. A missing association returns
invalid_request with the existing setup owner: an administrator uses
bind_native_repository with the observed integration expected_revision,
repository and source: "runner_checkout". The association supplies the
repository and clone URL to authorized runners, which clone using their own Git
credentials and report the matching origin. The integration read returns
checkout_verified once an active runner has reported that checkout recently;
a binding alone does not verify it. Cloud needs no repository credentials and
GitHub tracker mode stays disabled. Choose a configured
nonterminal, non-dispatchable destination such as Backlog. Linked creation
records pending source context for existing runner intake, and repeated source
URLs reuse the native work item. Cross-repository requests retain their refusal.
Use Copy MCP setup prompt on the same page. It includes the exact endpoint, organization/project context, the same private key instructions, supported transport, initialization, paginated tool discovery and an authorized read. For a supported Codex Streamable HTTP client, private configuration can use:
[mcp_servers.detent]
url = "https://app.detent.cloud/organizations/ORG/mcp"
bearer_token_env_var = "DETENT_API_KEY"
Configure another client using its documented private bearer-header support.
Send JSON POSTs with Content-Type: application/json and
Accept: application/json, text/event-stream. Clients using 2026-07-28 send
per-request protocol metadata and matching MCP-Protocol-Version, Mcp-Method
and tool-call Mcp-Name headers; they use stateless POSTs without initialize or
a protocol session. Older supported revisions initialize, retain
Mcp-Session-Id and the negotiated protocol version, then send
notifications/initialized. See generic setup
for both lifecycle examples. tools/list returns the complete current catalog
by default; optional _meta["detent/toolsets"] selects groups. Call the
discovered work_list with {"project_id":"YOUR_GRANTED_PROJECT_ID","limit":20}.
Verify the result's organization/project. No browser state or OAuth login is
required. GET/SSE returns 405; self-hosted clients that require SSE or cannot send
private bearer headers are unsupported. Report this explicitly and stop.
An optional GET receiving 405 is supported by the tested SDK.
Codex connection and confirmation failures
Distinguish client startup, client confirmation and Hub authorization before changing the connection. These failures have different owners:
| Symptom | Cause and supported next step |
|---|---|
Environment variable DETENT_API_KEY for MCP server 'detent' is not set |
The process hosting MCP lacks the configured environment variable. A shared Codex app-server may have started before the project environment loaded. Loading direnv in a later terminal does not update that process. Start a separate client from the loaded project environment; do not restart a shared daemon serving other sessions. |
MCP tool call requires approval, but approval policy is never |
Codex rejected the call before contacting Detent because this tool still requires client confirmation. The user must select an interactive session to review it. A previously saved tool-specific approval can allow later calls without a prompt; repeating the refused call or reconnecting to Detent does not approve it. |
A Detent response reports 403 or insufficient scope/grants |
The request reached the Hub, which refused the current authority. Check the key scope, selected project and account grants through the existing Settings and connection reads. Client confirmation does not grant server permissions. |
For an interactive operator session, the user can run the following from a project whose direnv environment already supplies the private key:
direnv exec . codex --no-daemon --sandbox workspace-write --ask-for-approval on-request
These options are supported by Codex CLI 0.162.1; check codex --help for the
installed version. --no-daemon runs this session separately with the current
environment. The user handles Codex's confirmation prompt before the MCP call
reaches Detent. An agent running under a policy that forbids approval must not
change that policy to retry a refused operation. Noninteractive codex exec
can perform permitted reads with the loaded environment, but its ability to
read does not establish permission to execute write tools. Inspect the actual
session settings when resuming; do not assume the original sandbox or a changed
user request altered its confirmation policy.
In that interactive session, the user can choose Always allow for a specific
tool they intend to authorize on subsequent calls. Codex persists that selection
in its user configuration, normally ~/.codex/config.toml (or
$CODEX_HOME/config.toml). For example, selecting it for update_runner_routing
on the server named detent saves:
[mcp_servers.detent.tools.update_runner_routing]
approval_mode = "approve"
The general key is mcp_servers.detent.tools.<tool>.approval_mode. This records
the user's client preference for that tool; it grants no Hub role, key scope or
project access. Keep the selection specific to the intended tool rather than
approving every tool on the server. A fresh codex exec loads the user
configuration by default and can use the saved approval even with
approval_policy = "never", since that call no longer needs a prompt. Check for
a different CODEX_HOME, configuration overrides or --ignore-user-config if
the preference is absent. An existing session may retain its earlier settings;
start a separate session with the intended configuration and loaded environment
instead of restarting a shared daemon. See the
Codex configuration reference
for per-tool approval settings.
After connecting, verify the selected account and organization with
connection_info, then read the intended project before making an authorized
change. Detent executes authorized writes directly and returns their result;
there is no Detent approval URL to open or pending approval to release. Preserve
truthful tool annotations and scoped credentials. Do not relabel writes as
read-only, change approval settings without the user's authorization or broaden
grants to conceal a client failure.
See the Codex MCP configuration reference
for client settings and client confirmation and current authority
for the server contract.
A concise agent handoff is:
Configure your supported MCP client for the exact copied organization endpoint using the same expiring scoped key from Settings → API & MCP. Ask me to store it privately as DETENT_API_KEY; never include it in conversation, URLs, logs, screenshots, diagnostics or committed configuration. Use bearer JSON POSTs with no cookies or CSRF. For Cloud OAuth clients, use the connector steps above. If your self-hosted client cannot do this, say it is unsupported and stop. Use the lifecycle supported by your client, discover tools/list and call work_list for a project granted to that key. Verify the context and report success without secrets. Use local client permissions for sensitive-operation confirmation; the Hub executes authorized calls directly.
The official TypeScript SDK client used for verification was
@modelcontextprotocol/sdk 1.31.0. In a private client directory containing
that dependency, the following uses the same private environment key:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "detent-agent", version: "1.0.0" });
const transport = new StreamableHTTPClientTransport(new URL(process.env.DETENT_MCP_URL), {
requestInit: { headers: { Authorization: `Bearer ${process.env.DETENT_API_KEY}` } },
});
await client.connect(transport);
let cursor;
const tools = [];
do {
const page = await client.listTools(cursor ? { cursor } : {});
tools.push(...page.tools);
cursor = page.nextCursor;
} while (cursor);
if (!tools.some((tool) => tool.name === "work_list")) throw new Error("Authorized read unavailable");
const result = await client.callTool({
name: "work_list",
arguments: { project_id: process.env.DETENT_PROJECT_ID, limit: 20 },
});
if (result.isError) throw new Error("Authorized read denied");
await client.close();
Connection evidence and release verification
Pre-merge verification on October 1, 2026 used an isolated, ephemeral HTTPS shared entry with a provisioned real organization tenant, a trusted fixture CA, a browser-created 7-day Read key and one project grant. This exercises the real Cloud entry/proxy/assertion, API-token authentication, membership provider, project read and MCP handlers. The fixture provider replaces the external identity service; this is not production deployment evidence.
Both project and work-item GETs returned 200 with the shared key and no
cookies or CSRF. The SDK initialized, discovered 55 tools across all pages,
and work_list returned the authorized project. The copied API prompt's actual
curl examples also passed without browser state. Revoking the key denied the
next API GET and MCP request on the established session. Focused tests cover
current roles and grants, organization isolation, expiry, revocation, membership
removal/replacement, machine rejection, redaction and current authority.
Isolated Chromium verified the generated page at the existing bookmark, key creation/revocation, masked one-time secret, complete API/MCP URLs, both secret-free copied prompts, and desktop/mobile layout. Chrome DevTools was unavailable in this worker. Screenshots were captured after dismissing secrets.
Pending release acceptance: Detent owns integration; the Cloud release owner owns deployment and a repeat against the actual canonical Cloud organization endpoint. After deploying the PR head, create a temporary Read key for one test project on this page. Run the direct reads and a supported SDK/MCP client's initialize/discovery/read with that same key and no browser state; record only status, negotiated protocol, tool count and public organization/project context. Verify both copy actions and key management on the deployed page. Revoke the key and confirm subsequent requests fail. Do not record secrets or claim this production check passed from the fixture result alone.
For 401, check private key configuration, expiry and revocation. For 403 or a hidden 404, check current membership, role, key scope and project grants. Local and self-hosted daemon instructions remain expandable on the same settings page.
Native work-item list results
Native work_list accepts created_by as a principal ID or me. me selects
issues filed by the calling human, including through that human's personal key;
service keys and worker credentials cannot use me. The selector combines with
state, label, query and archived filters within the caller's authorized project.
work_item and work_list expose the available filer's display identity in
actor.display_name beside the stable principal ID.
Native work_list accepts item limits through 200 and returns summaries in
data.items with an opaque data.next_cursor when more matching items remain.
The requested limit is a maximum: the existing issue page owner may return fewer
items to fit its 64 KiB serialized summary/cursor budget, derived from the 256 KiB
tool-result limit and reserving room for the envelope and MCP serialization.
Pass the cursor with the same project, query, state and label; the limit may
change. Items group in configured lane order, then use priority and newest
activity within nonterminal lanes, or newest activity within terminal lanes,
with identifiers breaking ties. Matching identities and order remain fixed while
paging, even if items change; item details remain current. Start a fresh query to
observe new membership or ordering. Cursor authority and one-hour expiry remain
intact.
Both stdio and HTTP return the same shape.
Local GitHub snapshot lists use the same lane comparator, with the snapshot's
updated_at as activity. Their existing offset pagination reads the current
snapshot on each request.
Summaries retain stable IDs, identifiers, URLs, revisions, title, state, priority,
labels, assignees, timestamps and authorized dependencies/blockers. Their
omitted_fields is ["body", "linked_source", "provenance", "external_references", "change"]; the empty body and null external-reference
placeholders mean omitted detail, not an empty original resource. Use work_item
or work_export for complete authorized detail under their existing byte bounds.
Queries still search full authorized original bodies. A single summary whose
metadata exceeds the page budget returns the existing safe unavailable error.
Native work_bulk_export exports complete stored items and comments from one
project. It accepts states, labels, open and archived with the same OR/AND
and archive semantics as work_list. Pages default to 500 items, accept limits
from 1 through 500, and retain full bodies, confirmed contract sections,
authorized dependencies/blockers, linked-source snapshots, provenance and
external references. The top-level items, total, truncated, next_cursor,
generated_at and freshness describe the export. Pass the cursor with the same
project and filters; the limit may change. A page may contain fewer items to fit
its 16 MiB serialized budget. An individual item exceeding that budget is
refused explicitly; no item content is silently shortened. Item and comment
revisions expose later edits. Reads use stored data and never refresh GitHub.
This tool requires the same read scope and current project grants as work_item.
Existing tools retain their limits and response shapes.
Associate an existing release-pick PR
The project-scoped associate_release_pick MCP tool uses the supported
native release-pick delivery contract.
Supply the dependent work_item_id, its expected_revision, a stable
request_id, and the exact source/PR/release identities in release_pick.
The source dependency must already be terminal and its fix merged on develop.
The owner verifies the repository and commit provenance, opens an independent
native Change, and preserves all holds. The existing runner captures the exact
source, reviews, gates, and lands on the recorded frozen release branch.
The tool does not merge or deploy. Worker completion contracts do not grant
permission to call this operator mutation.
Native validation evidence
get_change defaults to the current immutable version and current review,
check and discussion records. Its summary always evaluates all stored current
gate evidence. read.version_id identifies the selected evidence version;
read.sections gives explicit completeness and next_cursor for versions,
reviews, checks and discussion. Follow each cursor with get_change using
the same project, work item and Change selectors. A cursor fixes the selected
version and record boundary and is rejected if the current head changes.
An omitted record is not passing evidence. section: "versions" traverses
historical versions excluding the selected version already returned by the
default read; version_id selects exact historical evidence. Existing
get_change_version reads remain available. Bounded Change reads retain policy
identity, digests, gate requirements and review/check expectations, while omitting
authored file contents, runtime configuration and workflow definitions.
read.policy_details_omitted discloses this projection; retrieve the full immutable
policy through get_change_version. Policy omission never establishes approval.
Bounded Change reads return command
output as UTF-8 excerpts of at most 4 KiB, setting output_truncated: true when
output is shortened and preserving any existing truncation flag. Verdicts,
criteria, findings, command results and receipt identities remain complete;
section completeness describes records, not the completeness of output text.
The canonical scoped Change HTTP GET without view=bounded retains the full
stored reviews and output; exact-version reads also retain stored output.
When a review cannot fit with its required envelope, a section: "reviews"
continuation returns fragment using the retained recovery record format:
scoped item, record and version identities, byte offset, total byte length,
SHA-256 and base64-encoded canonical stored JSON. Each fragment carries at most
64 KiB of record bytes. Follow read.sections.reviews.next_cursor on the same
Change until the record is complete; verify contiguous offsets, stable identity,
length and digest before using its review. Fragmented records retain full command
output. Section completion describes traversal progress and never makes a partial
record passing evidence. Change cursors retain their selected Change, version,
head and history boundary, including for a non-latest Change on the same item.
Other unrepresentable records or envelopes still return change_record_too_large
without record content or passing evidence. Every serialized result remains
within the existing 256 KiB budget.
Native workers use the same projection through the Change HTTP read with
view=bounded and the same section, cursor, version and limit selectors.
Native work_attempt_receipt includes a bounded validation_audit projection of
recorded local commands and project scheduled failure occurrences. A work_item
whose body contains scheduled check evidence includes only that item's scheduled
reports and their candidate comparisons. Ordinary work-item and bounded Change
reads omit this audit; their own content and command receipts remain available.
Scheduled failures retain their existing issue/comment owner;
comparisons name those source records, their authors, and the run, job, attempt, Change and
version identities. The projection reads the selected project's database only
and makes no forge requests. partial identifies truncated records or candidate
comparisons; missing historical receipts, scope or environment remain unknown.
Command receipts retain the tested head/tree, result and duration. Commands
that emit detent-check-evidence records also supply their declared check scope,
command, Go host OS/architecture/toolchain and per-check result. Check durations
emitted by the shell recorder identify their one-second resolution. Dirty source
observations cannot establish the tested tree. The scheduled reporter retains
these records under the existing occurrence identity rather than substituting
the finalizer's environment for a failed job's environment.
A local-pass/scheduled-failure comparison requires identical recorded trees, matching scope and command, matching observed environment, and a local pass recorded before the failed check. Environment equality covers the recorded fields only. Integrated commit equality is reported separately: an earlier Change is only a candidate, and inclusion or causality is never inferred from ancestry. Local output is omitted from the comparison projection and private command text is redacted. Correlation grants no workflow authority, introduces no merge requirement and does not change any project's check policy.
Hosted application context reads
Organization productivity is available through the hosted organization
GET /reports?scope=organization&project=all&range=30d read and the existing reports MCP tool
with {"scope":"organization","range":"30d"}. Presets are 24h, 7d,
30d and 90d; REST custom windows use inclusive UTC from/to dates,
and MCP accepts explicit timestamps aligned to complete UTC hours. Presets use the same
calendar window resolution as project Reports. Both return an equal-length
previous window. Buckets are hourly for short windows and daily for the 7d, 30d and 90d
presets, and custom windows at least six days long.
The organization result contains current and previous productivity metrics,
projects with the same metrics per project, series and previous_series
with filed/completed counts and project splits, and filed_by/completed_by
counts for both periods. Completion cost includes lifetime usage before each
window's end for issues currently Done whose close falls inside that window.
Rework uses the project Reports unique-worked-issue denominator and excludes
infrastructure returns from its numerator. Missing cost or rework denominators
and unobserved cycle times return null rates or durations.
The labels, priorities, types, filed_by, runners and projects
filters accept arrays in MCP and repeated query parameters in REST. Dimensions
combine with AND; values within a dimension combine with OR. Project filters
intersect current readable grants and cannot expand access. A single MCP
project_id further narrows organization scope. Omitting REST scope retains
the existing Reports client response, including for project=all. Project-scoped
Reports retains its existing response. Organization reads use typed background-maintained
summaries through the reader pool; they may lag writes and never parse attempt
payloads or replay event history on the request path. Filer kinds are people,
agents, automation and unknown; completion attribution uses the final
attempt's enrolled runner or the human who recorded the Done transition, with
unknown retained when identity is missing.
Hosted dedicated and shared connections expose app_bootstrap_payload,
app_updates, release_notes, and hosted_events through the same adapter on HTTP and stdio.
app_bootstrap_payload takes {} and returns the current organization/directory,
actor, readable projects and workspace capabilities, support context, features,
coordinator availability, model/effort/access preferences, plan summary and hub
version. Browser CSRF, form tokens and session credentials are omitted. API keys
see only their current project grants and model catalogs.
app_updates takes {} and returns hub, minimum-runner and client builds, runner
versions, online/behind/refusal context and the behind count. It requires a
non-viewer member with runner management on every organization project and an API
key, when used, that can read every project. Removing a role or grant takes effect
on the next call and discovery; an old catalog grants no access.
release_notes takes optional limit (default 20, maximum 100) and cursor.
It returns the embedded builds newest first, entries, internal counts, running
and operator-patch flags, per-user unread state, has_unread, and an optional
next_cursor. Viewers can read it. It shares the hosted release-notes API owner
and performs no external requests; marking seen uses the authenticated browser
API. Bootstrap also includes has_unread.
hosted_events takes project_id, optional workspace_id, and optional cursor.
It returns one current collaboration invalidation sequence, an optional safe
workspace resource, observed_at, freshness: "current_observation", changed
and a cursor bound to that organization/project/workspace. The first observation
sets changed: true; sending its cursor again sets it to false when both observed
revisions match. A project activity or workspace revision change sets it to true.
This is a current invalidation observation, not event history: callers can use the
existing work reads to refresh affected content. No heartbeat, SSE stream or
background polling is exposed. Each call checks current project/workspace access;
a cursor grants no authority. Use a new observation when changing scope.
These reads reject unknown fields and requests over 64 KiB. Identifiers are at most 256 bytes and event cursors at most 2048 bytes. Project, organization-choice, model-choice and runner lists are at most 200 items and all results at most 256 KiB; an oversized complete result returns a safe unavailable error instead of truncation. Disabled optional services are explicit in bootstrap; a required unavailable service returns no application data or raw diagnostics. Hosted context is omitted from self-hosted and credential maintenance discovery. Parent #3259 generic-client and zero-pending acceptance remains with the parent owner.
Deployment MCP smoke
After the origin smoke, deploy-staging.yml and deploy-production.yml run the
MCP call pass against a dedicated smoke tenant on staging.cloud.detent.build
and cloud.detent.build. The operator provisions, in each environment, a
dedicated org owned by the platform account with one native throwaway project,
and stores a key restricted to that project as the environment secret
DETENT_SMOKE_API_KEY. Set the environment variable DETENT_MCP_URL to the
exact MCP URL copied from that tenant's API & MCP setup. The probe refuses any
other origin for the environment, discovers the project ID through
list_projects, and refuses a key that can see more than one project. Do not
store the key or project ID in source or issues.
The project needs a permitted transition between two nondispatchable,
nonterminal states. The probe creates one item per deployment and reuses a
separate dependency fixture. It checks edits, comments, dependencies, movement,
queue operations, unchanged integration settings, and archive/restore revisions.
Provision the persistent resources using deployment MCP fixtures
once in each environment. The smoke discovers the conversation by title and
the attachment, change, artifact receipt and attempt through the stable
MCP smoke dependency item. Discovery reads run before their resource detail
calls; missing fixtures produce explicit read skips with recorded reasons.
Unsafe external and operator mutations are explicitly skipped. The two GitHub
import reads are also skipped with a named reason: a native smoke project has
no external tracker import.
The operator-only get_change, get_change_version and get_native_run detail
reads are skipped because they exceed the dedicated project key's authority.
Available fixture reads run, and tool errors still fail deployment.
runner_project_diagnostics selects the dependency attempt's runner explicitly,
including when multiple runners report the project.
get_project_policy accepts either an approved policy or the documented
policy_mismatch refusal beginning with No approved repository policy;
other refusals fail the smoke.
Native projects refuse order_item and set_queue_priority with
invalid_request: Queue ordering and priority overrides require a GitHub-compatible project; native projects use issue priority. These commands
remain available for GitHub-compatible projects. A project without a cutover
receipt returns not_found: No cutover receipt exists for this project.
get_change_review_policy returns not_found: No approved Change review policy exists for this project until a policy is approved. Missing session
activity returns not_found: No recorded session activity exists for this work item and attempt from board_session_history. The smoke accepts only
these named refusals for these tools; other errors still fail deployment.
When integration reads report automatic native intake, the unchanged-settings
update omits intake to preserve it under the update contract.
Tools that are known to fail are listed in smokeKnownGaps in
tools/cifailure with their owning issue. They are reported but do not fail the
deployment. Any other failure fails it, and so does a listed tool that starts
passing, so the list only shrinks. An absent secret fails the smoke. Failures
use the existing High deployment failure reporter, and the deployment artifact
retains each tool's result.