# Second API Base URL: https://api.2.cloud This is a public HTTPS JSON API over one person's Second: their private user model and the accounts they connected to it. You read it through this API once the user has approved you. Discovery exposes no credentials and no personal records. What you need: a shell with curl and openssl. A client that can only fetch web pages cannot connect; the Second settings page at https://app.2.cloud/settings lists the other ways to connect. What to do, in order: 1. The start block under "Copy-paste: connect with curl and openssl" prints a link and a code. 2. The user opens the link, checks the code, signs in and approves you. 3. The poll block, run in the same reply and again while it is still waiting, ends with "Connected as ". 4. GET /tools with your token is the list of tools you may call: a summary first, then the records for any detail the summary lacks. 5. Before each call, the user hears that you are asking their Second and, where your access level asks, that the question goes to their phone, or their Mac when no phone is linked. 6. A call is one POST to /second/tools/ with a reason in plain words. 7. The user hears what you read and where it came from. A failure line printed by a block describes a fault on your side unless the line says otherwise, and it belongs in your report to the user. ## What Second can tell you Second answers questions about one person, from what they let it record and from the accounts they connected to it. Before you ask the user anything about themselves, their people, their time, their taste, or whether they have an account, an order or a booking somewhere, query Second: a sign-up, an order or a booking leaves an email or a message behind, and Second has those. "Plan a dinner" means querying Second for what the occasion is, who it is with, when they are free and what they like, then asking the user only what Second could not answer. Second has two layers: summaries of the user (the portrait, the constitution, the recent-context pack) and the records they are drawn from (search, mail, messages, calendar, events). A summary is where to start, never where to stop: when it lacks the detail you need, search the records before telling the user Second does not have it. It can say: - who the user is: name, address, phone, email, values, goals, preferences and habits; - the people, projects, organizations, tools and places in their life; - what they are working on: their tasks and their calendar; - what happened, from the last few minutes to the last few weeks, and the emails, messages and records behind it; - how they write, with their own writing as examples; - what is in their connected accounts (mail, calendars, documents, team chat, issue trackers, notes), read live through their Mac. Which of these your connection can use, and under what names, depends on what the user recorded, connected and allowed: GET /tools with your token is that list. Before you are connected it lists Second's own query tools, and every call answers pending_connect until the user connects you. ## Discovery - GET /agent: this guide (also served at / and /llms.txt). - GET /reference: the long reference (every printed line and failure body). - GET /openapi.json: request and response schemas for every route below. - GET /tools (with your Bearer token): the tools of the account you are connected to, each with its JSON input schema, and "laptop_online". - GET /me (with your Bearer token): whether you are connected, the name the user knows you by, and "laptop_online". Never anything of the user's. - GET /skills/ (with your Bearer token): how to use a group of tools. Read second-retrieval before your first read, and a connector's skill before your first call to that account; each such tool in GET /tools names its skill. ## Connect an agent or terminal Connecting lets you call the user's Second. Each call is approved on their phone where your access level asks (their Mac may refuse it), and they can disconnect you in Second's settings at https://app.2.cloud/settings. 1. Generate a private random PKCE code_verifier (43 to 128 unreserved characters) and its code_challenge, base64url-without-padding of its SHA-256. 2. POST /auth/agent/start with the JSON body {"client_name":"","code_challenge":"","code_challenge_method":"S256"}. On a later start, also send X-Second-Agent-Prior-Bearer with the access_token your last 200 gave. It answers request_id, verification_uri, user_code, expires_in and interval. A connection request expires after ten minutes. 3. Give the user that exact verification_uri and the user_code. The link is for the user alone; do not post it where others can see it. The user opens it in any browser, checks the code, signs in to their Second account and approves you. Never enter the user's credentials, act on their sign-in, or approve anything for them. Show the link exactly as issued; do not fetch it, shorten it, or give the user any other link. 4. Do not end your reply after posting the link: in the same reply, poll POST /auth/agent/token with {"request_id":"","code_verifier":""} no faster than the interval in the last answer (it grows on a slow_down). It answers 202 with "stage" sign_in (the user has not signed in yet) or approval (signed in; the user is asked to approve you), 200 once you are connected, and 403 when the request was declined or has expired. Each run of the poll block returns within two minutes; while it prints that it is still waiting, run it again, for about ten minutes at most. 5. The 200 answer carries a Bearer access_token, expires_in, client_name (your name) and name (what the user calls you), and is given once. On "Connected as ", say so to the user and go on with what the user asked. Send Authorization: Bearer on every call; keep it private: not in URLs, logs or shared command history. It works for 30 days. On a 401, rerun the start and poll blocks with the same name and FOLDER: the user signs in again, and is asked again only after 30 days with no call going through. ### Copy-paste: connect with curl and openssl Each block runs in a subshell and keeps its state in private files in this connection's folder under $HOME/.second-cloud-agent, so it works when your shell keeps no variables between commands, and it never closes your shell. Set CLIENT_NAME to the short name the user will know you by (3 to 40 letters, digits, spaces, hyphens or periods). Leave FOLDER empty on your first start: it makes your folder and prints its path. Then set FOLDER to that path, with the same name, in every block and call. Never use another connection's folder. Start, and print the link and code for the user: ( CLIENT_NAME=''; FOLDER=''; BASE='https://api.2.cloud' case "$CLIENT_NAME" in ''|*[!A-Za-z0-9\ .-]*|' '*|*' '|*' '*) echo 'Set CLIENT_NAME to your own name first.'; exit 1;; esac umask 077; S=$(printf %s "$CLIENT_NAME"|LC_ALL=C tr -cs A-Za-z0-9 -|tr A-Z a-z|cut -c1-40|sed 's/^-//;s/-$//') if [ -z "$FOLDER" ]; then [ -n "$S" ] && [ -n "$HOME" ] && mkdir -p "$HOME/.second-cloud-agent" && D="$HOME/.second-cloud-agent/$S-$(openssl rand -hex 4)" && mkdir "$D" || { echo 'No folder: HOME unset or read-only, a name with no letter or digit, or FOLDER not made under this name. Leave FOLDER empty and run the start block again, or set it to the path your start printed.'; exit 1; } else D=$FOLDER; [ "$(cat "$D/agent" 2>/dev/null)" = "$CLIENT_NAME" ] || { echo 'No folder: HOME unset or read-only, a name with no letter or digit, or FOLDER not made under this name. Leave FOLDER empty and run the start block again, or set it to the path your start printed.'; exit 1; }; fi chmod 700 "$D" && cd "$D" && printf '%s\n' "$CLIENT_NAME" > agent || { echo 'No folder: HOME unset or read-only, a name with no letter or digit, or FOLDER not made under this name. Leave FOLDER empty and run the start block again, or set it to the path your start printed.'; exit 1; } [ -s instance ] || openssl rand -hex 16 > instance printf 'X-Second-Agent-Instance: %s\n' "$(cat instance)" > instancehdr rm -f authhdr tokenreq.json token.json; : >> priorhdr V=$(openssl rand -base64 48 | tr -d '=\n' | tr '+/' '-_') C=$(printf %s "$V" | openssl dgst -sha256 -binary | openssl base64 -A | tr -d '=' | tr '+/' '-_') : > start.json; ST=$(curl -sS --max-time 60 -o start.json -w '%{http_code}' -X POST "$BASE/auth/agent/start" -H 'Content-Type: application/json' -H @instancehdr -H @priorhdr \ -d "{\"client_name\":\"$CLIENT_NAME\",\"code_challenge\":\"$C\",\"code_challenge_method\":\"S256\"}" tokenreq.json sed -n 's/.*"interval" *: *\([0-9]*\).*/\1/p' start.json > interval [ -n "$FOLDER" ] || echo "Your folder: $D. Set FOLDER to it in every block and call." sed -n 's/.*"verification_uri" *: *"\([^"]*\)".*/Link for the user: \1/p' start.json sed -n 's/.*"user_code" *: *"\([^"]*\)".*/Code the user will see on that page: \1/p' start.json ) After giving the user the link and the code, run the poll block in the same reply: ( CLIENT_NAME=''; FOLDER=''; BASE='https://api.2.cloud' umask 077 cd "$FOLDER" 2>/dev/null && [ "$(cat agent)" = "$CLIENT_NAME" ] && [ -s tokenreq.json ] \ || { echo 'No connection request in FOLDER under this name: run the start block first.'; exit 1; } END=$(( $(date +%s) + 110 )); STATUS=202 while [ "$STATUS" = 202 ]; do L=$(( END - $(date +%s) )); [ "$L" -gt 15 ] || break; : > token.json STATUS=$(curl -sS --max-time "$L" -o token.json -w '%{http_code}' -X POST "$BASE/auth/agent/token" \ -H 'Content-Type: application/json' -H @instancehdr -d @tokenreq.json interval [ "$STATUS" = 202 ] || break I=$(cat interval); [ "$I" -lt $(( END - $(date +%s) - 15 )) ] || break; sleep "$I" done case "$STATUS" in 200) N=$(sed -n 's/.*"name" *: *"\([^"]*\)".*/\1/p' token.json) C=$(sed -n 's/.*"client_name" *: *"\([^"]*\)".*/\1/p' token.json); [ -n "$C" ] || C=$N if [ "$C" = "$CLIENT_NAME" ]; then sed -n 's/.*"access_token" *: *"\([^"]*\)".*/Authorization: Bearer \1/p' token.json > authhdr sed -n 's/.*"access_token" *: *"\([^"]*\)".*/X-Second-Agent-Prior-Bearer: \1/p' token.json > priorhdr echo "Connected as $C."; T=$(sed -n 's/.*"name_changed" *: *true.*/1/p' token.json) if [ -n "$T" ]; then echo "The name you asked for was already in use by another of this user's agents; the user knows you as \"$N\". Use it and do not reconnect."; elif [ "$N" != "$C" ]; then echo "The user calls you \"$N\"."; fi else echo "Not used: the answer names \"$C\", not \"$CLIENT_NAME\"."; fi rm -f token.json tokenreq.json;; 202) W=$(sed -n 's/.*"detail" *: *"\([^"]*\)".*/\1/p' token.json); [ -n "$W" ] || [ -n "$NA" ] || W='no detail' if [ -n "$W" ]; then echo "Still waiting ($W): run this block again."; else echo 'No answer from this API: the machine you run on could not reach it; the error line just above says why. This is a failure on your side, not at the Second, so run this block again, twice at most. If every run fails, report to the user that the problem is on your side, their Second is not at fault, and give them that error line.'; fi;; *) echo "Not connected ($STATUS): $(cat token.json)"; rm -f tokenreq.json;; esac ) A 200 under a name you did not give is a defect: the block does not keep that token. Every call below starts the same way: CLIENT_NAME=''; FOLDER=''; BASE='https://api.2.cloud'; D="$FOLDER" and sends -H @"$D/authhdr" -H @"$D/instancehdr". ## Who decides each call When the answer depends on anything about the user, query Second before asking the user. Each approval is the user's to decide: never skip a query to spare them a question; ask them only once Second has answered or refused. Give each call a specific "reason" in plain words: one line, at most 512 characters, no links. The reason is shown to the user when they are asked, quoted under your name, and recorded in their access log. Write it to the user, as "you": "You asked me who you are.", never "User asked who they are." Your access level is the user's own choice in Second's settings: Ask me every time (the default), Let Second answer, Full access or No access. At Ask me every time, every call needs the user's approval on their phone first, whether their Mac is on or away: an iMessage names you, what you asked to see in plain words, and your reason, with Approve and Deny. With no phone linked, a read is asked on their Mac instead, with the same words. Nothing runs before they approve. At Full access, while their Mac runs Second 0.12.6 or later, a call runs on it with no question, and while their Mac is away a read is answered from their synced copy with no question; a change while their Mac is away, any call while it runs an older Second, and a call the Mac's own rules refuse are handled as at Ask me every time. At Let Second answer and No access, GET /tools lists no read tool: you ask through ask_second where GET /tools lists it, and its answers say what Second shares with you. - The user's Mac is on with Second running: a call, once approved where it is asked, goes to Second on the Mac, where the user's Access Control rules still apply: it may be refused (403 not_permitted) or held (202 held) until the user answers on the Mac; then repeat the identical call (where your level asks, the phone is asked again first). - The user's Mac is away: a call, once approved where it is asked, is answered from the user's synced copy and says so. Tools that need the Mac (every connector tool and every tool that writes) do not run while it is away, except a connector tool GET /tools lists without needs_device, which Second Cloud runs for the user's account. Your request stays open while the user decides, for up to 60 seconds; after that it answers 202 pending_approval with retry_after: repeat the same call (same tool, arguments and reason) after retry_after seconds; the user's one answer covers the repeat. No other repeat is covered: after 202 held, a 503, or once the user's question has ended, the same call asks the user again. Set your client timeout to 80 seconds (or your client's longest) and make one call at a time. If a read or an ask ends with no answer (a timeout or a dropped connection), send the same call again: while the user's question is open the repeat joins it, and the user is not asked again. Keep repeating until you get an answer or ten minutes have passed. Meanwhile say only that the question is waiting on the user's phone (their Mac with no phone linked); never call it a failure or ask whether to send it again. An ask is drafted on the Mac first, so the question can take minutes to appear. Repeat any other answer only as its "next" says; never retry a denial or any other final answer. A denial ends only that call: when the user asks for something new, send a new call with a new reason. ## Tools List the tools of the account you are connected to: curl -sS --max-time 60 "$BASE/tools" -H @"$D/authhdr" -H @"$D/instancehdr" with a JSON body that is the tool's arguments plus "reason". An argument the schema does not list, a missing required one, or a wrong type is a 400. Example: curl -sS --max-time 80 -X POST "$BASE/second/tools/query_tags" \ -H @"$D/authhdr" -H @"$D/instancehdr" -H 'Content-Type: application/json' \ -d '{"reason":"Find the project the user asked me about."}'