JohnnyOne Partner API

Drive JohnnyOne programmatically — no UI. Create and control AI/terminal sessions and planner/development runs, and stream live terminal output, over two authenticated transports.

Base URLs use the real worker domain johnnyone.ethan-353.workers.dev (prod). All traffic is over TLS. For dev/qa use the corresponding -dev/-qa subdomain.

Two transports

TransportUse it forAuth
GraphQL https://johnnyone.ethan-353.workers.dev/graphqlLogin, session lifecycle CRUD, planner/development lifecycle CRUD, listing nodes & tools.Authorization: Bearer <accessToken>
WebSocket wss://johnnyone.ethan-353.workers.dev/api/relay/wsLive terminal I/O — subscribe to screen updates, send input, resize, kill.Authorization: Bearer <accessToken> on upgrade, or ?token=<accessToken>

Your account is a dedicated service account (one tenant + user). JohnnyOne hosts the desktop runtime for you — you never install anything. The server resolves your desktop node from your token; you never supply a node id.

Authentication

Log in once to get a short-lived accessToken (JWT) and a longer-lived refreshToken. Send the access token on every GraphQL request and on the WebSocket upgrade.

curl -s https://johnnyone.ethan-353.workers.dev/graphql \
  -H 'content-type: application/json' \
  -H 'x-tenant-id: your-tenant' \
  -d '{"query":"mutation($i:LoginInput!){login(input:$i){accessToken refreshToken}}",
       "variables":{"i":{"email":"you@partner.com","password":"…","tenantId":"your-tenant"}}}'

Use the token as a Bearer header on GraphQL:

Authorization: Bearer <accessToken>
Token lifetime. Access tokens are short-lived. When one expires, call refreshToken(input: RefreshTokenInput!) to get a new pair (returns { accessToken refreshToken }). A WebSocket authenticates once at upgrade — if the token expires mid-connection, reconnect with a fresh token (see Errors & limits).
?token= fallback. Browser WebSocket clients can't set headers, so the token may be passed as a query param. Only over TLS, only with a short-lived access token, and don't log or persist the URL.

GraphQL API

Endpoint: POST https://johnnyone.ethan-353.workers.dev/graphql. The schema is introspectable — point a GraphQL client at it to discover every type and argument. The partner-usable operations are below.

Terminal / AI sessions

OperationSignature
CreatecreateAiSession(input: CreateAiSessionInput!): AiSession!
ListlistAiSessions(status: String): [AiSession!]!
GetgetAiSession(id: ID!): AiSession
RenameupdateAiSessionTitle(id: ID!, title: String!): AiSession!
Set working dirupdateAiSessionWorkingDirectory(id: ID!, workingDirectory: String!): AiSession!
Set providerupdateAiSessionProvider(id: ID!, provider: String!): AiSession!
ArchiveupdateAiSessionArchived(id: ID!): AiSession!
DeletedeleteAiSession(id: ID!): Boolean!
List messageslistAiMessages(sessionId: ID!, limit: Int, offset: Int): [AiMessage!]!

CreateAiSessionInput: { title?, provider?, model?, workingDirectory? }. The returned AiSession.id is the sessionId you use on the WebSocket.

listAiMessages returns messages for a session (use with listAiSessions / getAiSession).

curl -s https://johnnyone.ethan-353.workers.dev/graphql \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"query":"mutation($i:CreateAiSessionInput!){createAiSession(input:$i){id title provider workingDirectory}}",
       "variables":{"i":{"provider":"claude_code","workingDirectory":"/home/you/project"}}}'

Planner / development runs

OperationSignature
CreatecreateAgentPlan(input: CreateAgentPlanInput!): AgentPlanRun!
StartstartAgentPlan(id: ID!, phaseId: String, phaseRunMode: String): AgentPlanRun!
ListlistAgentPlans(status: String, runType: String, onlyExisting: Boolean): [AgentPlan!]!
Get (with phases/tasks/events)getAgentPlan(id: ID!): AgentPlanRun!
RenameupdateAgentPlanTitle(id: ID!, title: String!): AgentPlanRun!
AmendupdateAgentPlanAmend(id: ID!, brief: String!): AgentPlanRun!
StopupdateAgentPlanStopped(id: ID!): AgentPlanRun!
App scopeupdateAgentPlanAppScope(id: ID!, appScope: String): AgentPlanRun!
BlockupdateAgentPlanBlocked(id: ID!, reason: String!): AgentPlanRun!
DeletedeleteAgentPlan(id: ID!): Boolean!

CreateAgentPlanInput: { runType?, title?, workspacePath!, planPath!, workerProvider!, reviewerProvider!, brief?, appScope?, docsScope?, referencePaths? }. getAgentPlan returns an AgentPlanRun with plan, phases, tasks, and events — poll it (or watch agent_plan_run_updated on the WebSocket) to track progress.

Discovery helpers

listDesktopNodes (your online desktop node) · listDetectedCliTools (available providers/CLIs on your host).

Live terminal (WebSocket)

Connect to wss://johnnyone.ethan-353.workers.dev/api/relay/ws with your token. The server validates the token, resolves your desktop node, and routes the socket. Messages are JSON envelopes:

{ "type": "<envelope type>", "sessionId": "<AiSession.id>", "data": <payload> }

Client → server

typePayloadMeaning
terminal_visual_subscribe{ sessionId }Start receiving terminal_screen updates for the session.
terminal_command{ sessionId, data: "<input>" }Send keystrokes / input to the terminal.
terminal_resize{ sessionId, data: { cols, rows } }Resize the terminal.
terminal_visual_unsubscribe{ sessionId }Stop receiving updates.
terminal_kill{ sessionId }Terminate the terminal session.

Server → client

typePayloadMeaning
terminal_screen{ sessionId, data: <screen> }Current terminal screen contents (rendered output). Sent on subscribe and on change.
terminal_command_ack{ sessionId }Input was accepted.
session_deleted / session_updated{ sessionId }Session lifecycle change.
agent_plan_run_updated{ data }A plan run advanced (phase/status change) — handy for live plan progress.
Ownership. You may only act on sessions your account owns; an envelope referencing another account's sessionId is rejected. You never send a node id — the server derives your node from the token.

Quickstart (Node.js)

Login → create a session → stream its terminal → send a command. Requires ws (and Node 18+ for fetch).

import WebSocket from 'ws';
const BASE = 'https://johnnyone.ethan-353.workers.dev';
const gql = (q, variables, token) => fetch(BASE + '/graphql', {
  method: 'POST',
  headers: { 'content-type': 'application/json', ...(token && { authorization: `Bearer ${token}` }) },
  body: JSON.stringify({ query: q, variables }),
}).then(r => r.json());

// 1. Login
const { data: { login } } = await gql(
  'mutation($i:LoginInput!){login(input:$i){accessToken refreshToken}}',
  { i: { email: 'you@partner.com', password: process.env.PW, tenantId: 'your-tenant' } });
const token = login.accessToken;

// 2. Create a session
const { data: { createAiSession: s } } = await gql(
  'mutation($i:CreateAiSessionInput!){createAiSession(input:$i){id}}',
  { i: { provider: 'claude_code', workingDirectory: '/home/you/project' } }, token);

// 3. Open the live terminal
const ws = new WebSocket(`${BASE.replace('https','wss')}/api/relay/ws`, {
  headers: { authorization: `Bearer ${token}` },   // or append ?token= in the URL
});
ws.on('open', () => {
  ws.send(JSON.stringify({ type: 'terminal_visual_subscribe', sessionId: s.id }));
  ws.send(JSON.stringify({ type: 'terminal_command', sessionId: s.id, data: 'echo hello\r' }));
});
ws.on('message', (buf) => {
  const msg = JSON.parse(buf.toString());
  if (msg.type === 'terminal_screen') console.log(msg.data);
});

A Python equivalent uses httpx/urllib for GraphQL and websockets for the socket — same envelopes.

Errors & limits

Versioning & forward-compat

The endpoint is unversioned and additive. Select only the fields you need; tolerate unknown enum values and extra response fields.

For errors use the real signals: GraphQL errors[].message text, WSS upgrade HTTP status 401/503, and in terminal_command_ack the .error:'forbidden_session' field. (The published contract does not emit extensions.code values such as UNAUTHENTICATED/FORBIDDEN_SCOPE/RATE_LIMITED.)

Changelog

Full policy: see the inlined summary above and the runbook (deployed alongside). All changes additive unless removal sweep with deprecation.

For AI agents

JohnnyOne Partner API · integration guide. Operations and envelopes reflect the live GraphQL schema (worker/schema/*.graphql) and relay protocol.