Machine authorization
Cloud and self-hosted servers share the device-code wire contract in
yoke_contracts.machine_authorization. Cloud retains its org selection. Self-host binds a code to its sole organization at start.
Connect
Run yoke connect https://<server> and compare the four-character Match code
with your own machine's request. Approve it from a machine already connected to
that server with yoke machines approve K7F2, or use Machines → Waiting for
approval in the signed-in workbench. Approve only your own codes: approval
grants your identity. The wizard's A team server discovers the sign-in method.
Without company sign-in, teammates choose Enter invite code and enter the
private code from their administrator. For bootstrap or recovery, use
yoke connect https://<server> --token-stdin or --token-file PATH;
the first-boot admin token connects the first machine.
Interactive yoke connect announces and opens browser approval; approve, then
return to the terminal. Agent, non-TTY, and --json runs print the link instead;
--json sends instructions to stderr. Use the printed link if opening fails.
Wire
POST /api/machine/authorizationsreturnsdevice_code,user_code,verification_uri,verification_uri_complete,expires_in, andinterval. Every mode sendsmachine_id(the configured UUID),machine_name(the operating system display hostname, at most 150 characters), andplatform(for examplemacOS,LinuxorWindows, at most 80 characters). All three are required byMachineAuthorizationStart; the browser reads these facts before the first poll. Start with the current installer against a server serving this request contract; a mismatched contract refuses by name.POST /api/machine/authorizations/tokensendsdevice_code,machine_id, andmachine_name. Success returnstoken,org, andapi_url.- Retry HTTP 202
authorization_pending, HTTP 503machine_credential_unavailable, and HTTP 429authorization_poll_rate_limited(honoringRetry-After). HTTP 410authorization_denied,authorization_expired, orauthorization_consumedrequires a new connection. Other statuses are diagnosed refusals. - Cloud's API authority is
/api/orgs/<slug>on the selected origin. Self-host's authority is the server origin. Clients reject foreign origins or mismatched authorities before persisting a credential. - Self-host discovery is
GET /api/machine/authorizations:device_codeis true on every current server, andcompany_sign_inis true exactly when company sign-in is configured. Without company sign-in, codes and delivered credentials name the request's own origin. A server that omitscompany_sign_inpredates it and offered device codes only with company sign-in. Partial configuration refuses asoidc_misconfigured; an older server without discovery requires an explicit API token until its operator upgrades it. The wizard retains the discovery refusal and offers Use API token, Edit connection, and Choose another home, including when company sign-in is misconfigured.
Approval and delivery
yoke machines pending lists code, machine, requested-by identity and expiry for
the signed-in organization. yoke machines approve CODE and
yoke machines deny CODE show the viewer and machine before recording a decision;
codes accept either case. --json emits structured function replies. A local
universe instead explains that other computers cannot connect and points to
yoke self-host init.
The CLI and shared /machine-approval[/CODE] workbench page use
machine_authorization.get and machine_authorization.resolve. The read returns
authorization with code, machine name/ID, platform, expiry, decision, current
state, viewer and requested-by identity; resolve takes {code, action: approve|deny}
and returns that decision. The pending function is machine_authorization.pending.
Hosted routing selects the organization and hands its code to the engine store.
Only an active human org member can approve their own machine. The first decision binds the code to that person; another actor cannot take it over. An identical decision can be repeated, while an opposite decision refuses. Credential issuance remains engine-owned atomic machine registration/rotation. Consumption and issuance commit together; one poll receives the raw credential and no raw credential is stored in the code record. Network loss after delivery requires starting a new code.
The self-host store keeps hashed device secrets and expires codes after ten
minutes. An approval made in time stays collectable ten more minutes (an agent
setup call collects it next call); later, poll refuses authorization_expired
naming approval_uncollected and inspection reports state uncollected. A
later start deletes records past that window. Trusted transport client identity
bounds pending admission: eight live codes per client, 128 per server; a new
machine UUID or forwarding header creates no new budget. Past it,
authorization_client_capacity and authorization_capacity refuse starts;
finish an approval or retry after expiry.
Persistent atomic counters are shared across processes and restarts, separately
from telemetry: six start requests and 120 poll requests per client per minute.
HTTP 429 authorization_start_rate_limited or authorization_poll_rate_limited
includes Retry-After. Start refusals require retrying after that delay. The CLI
waits through poll throttling and polls the same code after Retry-After, bounded
by its expiry and cancellation. Missing or invalid delays retain the normal poll
interval; capacity refusals remain fatal. Failed and malformed
requests also consume the budget. For self-host, set YOKE_API_TRUSTED_PROXIES
in the bundle's .env to the TLS proxy IPs/CIDRs, then restart with
yoke self-host init --dir PATH --protect-existing --start so the server sees
each forwarded client. Headers from undeclared peers are ignored; clients
sharing an address share these budgets. A denied, consumed,
or expired code requires a new connection. Wrong machine identity, missing
org membership, or disabled actor refuses with its recovery step. A database
failure before issuance leaves delivery retryable. Cookies authorize browser
functions only with the workbench's existing same-origin CSRF protection.
Waiting for approval
Machines shows Waiting for approval above the roster when requests are
waiting. Match the code to your terminal, then choose Review to open the
shared approval card inline. The URL becomes /machine-approval/CODE without
adding a history entry. Approve only a machine you are connecting yourself.
The list shows its name, full machine ID, requester when known and local expiry;
an unbound request says Not signed in yet. Expired rows cannot be reviewed.
The approval route without a code lists waiting requests without an entry form.
A selected link shows Other machines waiting below its card. Lists refresh
every 30 seconds while a card is open and when the page becomes visible; after
a decision the next read removes the row and keeps the card's outcome visible.
An empty approval route teaches yoke connect <server> on the new machine.
Roster or relay failures leave pending requests usable. A failed pending read
offers Try again; an older server directs you to the link your terminal
printed. Local mode shows the reachability notice without a pending read.
Retired and foreign-owned machines
Start refuses a machine_id the registry retired with HTTP 409
machine_retired, before any code exists, so nobody approves a code that
delivery would refuse. Hosted machine_authorization.handoff refuses the same
id with the same code before storing the hosted code. On that refusal yoke connect and the setup wizard
replace this computer's configured UUID with a new one in
~/.yoke/config.json and request a fresh code once, on every harness, so no
reinstall is needed. Both say which retired id they replaced; the retired
machine keeps its row and history on the Machines page. A second
machine_retired for the new id stops with that named refusal. A machine
retired after its code was issued is refused at delivery the same way: the
client swaps in a new identity and tells you to run yoke connect again. A
server older than this behavior issues the code and refuses only at delivery,
which takes that same path. On a local universe, yoke machine register and
setup's registration step make the same one-time swap.
Only retired machines get a new identity. An active machine registered to
another person still refuses as machine_owner_mismatch: its owner or an
administrator retires the machine, then you run yoke connect again on this
computer.
Approval page
The selected request shows one shared card headed Approve this machine. It shows the real machine name (omitted when absent), platform and full machine ID, uppercase Match code, Requested by when bound, organization, Approving as, and the viewer's local expiry time and zone. Identities use the supplied name and actor number; This machine will act as you explains the approval. An older server without viewer facts shows your signed-in account and asks its operator to upgrade. The UTC instant stays in the time element. Approve only if the code matches your terminal. Approve machine is the primary action; Deny authorizes nothing. The card can also be embedded beside a request list, where its heading receives focus. A direct approval link focuses Approve machine. Neither an absent URL code nor an unavailable code offers a manual code-entry form.
A first organization connection shows four setup steps, with Connect machine active and the preceding steps complete. The same visit shows every step complete after approval. Later connections omit the bar. This follows the organization's machine registry, without a caller-supplied first-run marker.
Approval shows Machine connected, a green success line, and a separate
orange Return to your terminal callout. The approved facts omit expiry
and the code-matching hint. A denied request says Nothing was authorized.
Expired requests show their local expiry when their stored facts remain
available; no client promises a replacement code automatically. Start a new
connection with yoke connect from the terminal. A consumed request says it
was already answered and directs you to Machines or a fresh connection.
Membership refusals name sign-in recovery, and a failed write keeps the actions
available for retry without reporting success.
An owner mismatch says the connection was already handled by the bound person,
using a name only when an existing read supplied it, otherwise another person
in your org. If that was not you, ask them or an admin to retire the machine,
then run yoke connect again. The refusal never discloses a new identity.
The installer and connect wait stop on expiry; neither rotates codes silently.
For a local universe, the page skips approval reads and explains its local-only
reach, even with a token. It gives yoke self-host init and the
Self-host guide.
Inspection may read an expired or consumed record to display its outcome, but resolve refuses expired codes and poll refuses consumed or unapproved expired codes. A record removed by pending-store expiry cleanup is unavailable; membership and ownership still protect reads. Light and dark themes share the same card and narrow layouts.
Decide as the signed-in actor
Use yoke machines pending, then yoke machines approve CODE or
yoke machines deny CODE. Check the returned decision and approver; finish
polling on the original machine. Caller-supplied owner, org, or credential
is never accepted.
On a hosted connection (https://app.upyoke.com/api/orgs/<slug>), the selected
organization scopes the request. See Hosted org administration.
An older server refuses a missing command with function_version_skew and its
minimum serving version. Connect with an explicit token using
yoke connect <server> --token-stdin, or ask the server operator to upgrade.
Shared schema
The yoke-contracts wheel ships yoke_contracts/machine_authorization.schema.v2.json,
generated from the request and response Pydantic models. Load it with Python's
importlib.resources.files("yoke_contracts").joinpath("machine_authorization.schema.v2.json"),
or read that path from the pinned wheel in another language. Validate a body
against its named $defs entry while retaining the document's other $defs
for references. The x-http section names the endpoint, request model, success
model, error outcome's HTTP status and body model, and retry policy. Start and poll requests are shared across hosting modes.
MachineAuthorizationStarted and MachineAuthorizationApproved define success.
MachineAuthorizationPending, MachineAuthorizationDenied,
MachineAuthorizationExpired (including consumed), MachineAuthorizationSlowDown,
and MachineAuthorizationUnavailable define poll outcomes. Other diagnosed
refusals use MachineAuthorizationRefused. Recovery text is optional on the wire: self-host supplies message, and Cloud
supplies detail. Both are explicit fields in the shared refusal model; the CLI
retains supplied recovery text in fatal diagnostics. Cloud credential refusals
use HTTP 409; missing machine identity uses 400, malformed requests use 400 or
413, and retryable credential unavailability uses 503. The strict schema rejects
unrecognized response fields. Secrets are excluded from model repr and
client validation errors. The CLI and self-host route use these models;
parse_authorization_response also checks known poll statuses. Origin and API
authority checks remain required after body validation.
Regenerate after changing models with
yoke dev run -- python3 -m yoke_contracts.machine_authorization_schema.
Check drift with the same command plus --check; the contract test also fails
on byte drift. A breaking wire change requires a new schema version and the
linked consumer's tests against the exact candidate wheel. The schema's v2
identifies its contract independently of the wheel's release version.
Shared client helpers are authorization_origin, same_origin_url,
credential_api_url, and parse_authorization_response, with
BROWSER_VERIFICATION_PATHS, POLL_OUTCOMES, and RETRYABLE_POLL_ERRORS.
Hosted lifecycle delivery accepts qualified instants or aware Python datetimes for occurrence and expiry. Numeric epochs, naive dates and empty supplied clocks refuse. Stored decision clocks are native; owned occurrence, expiry and end context fields and audit receipts use fixed-six UTC RFC3339Z or null. Expiry is an inclusive comparison of instants, independent of database session timezone.
Hosted code handoff
After sign-in and organization selection, Platform calls registered
machine_authorization.handoff in the selected organization's engine. Only
an active hosted_service identity holding hosted_service.deliver on the
explicit engine org_id may call handoff or machine_authorization.handoff_poll.
Human members and admins cannot call either function. Both registry and
client-readable floors are next-release until a release carries them; roll
the engine forward before enabling these calls in the hosted consumer.
Handoff takes {device_code_sha256, user_code, machine_id, machine_name, platform, expires_at, org_id}. The digest is lowercase hex SHA-256 of the
original device secret; never send the raw secret to handoff. user_code is
four characters from 23456789ABCDEFGHJKLMNPQRSTUVWXYZ, such as K7F2.
Inspect and resolve trim surrounding whitespace and accept lowercase input.
Other formats refuse authorization_code_invalid without changing the request;
run yoke connect on the connecting machine for a new code.
Machine ID is a UUID; name and platform are nonempty, at most 150 and 80
characters. Expiry is qualified RFC3339, after now and within the engine's
ten-minute code TTL. Org ID is the positive engine organization ID,
never Platform's registry ID or an inferred default.
Success returns {authorization: {user_code, expires_at, state: "pending"}}
with canonical UTC expiry. Identical pending replay returns the same row;
a changed or closed request under its digest refuses device_code_taken.
Another row holding the match code refuses match_code_taken. Issue a new
secret and match code on either collision. Expired input refuses
authorization_expired; malformed or excessive expiry refuses
authorization_expiry_invalid. An unknown engine org refuses
authorization_org_unknown; a wrong identity or org grant refuses
permission_denied. Correct malformed fields after authorization_refused
or authorization_handoff_invalid; no request is replaced on refusal.
Handoff uses machine_authorization_codes, with no requester actor until
a signed-in member decides. Inspect and resolve still use this record and
personal approval. Handed-off rows have no per-client capacity key; they
count toward the same 128 pending-code server limit. authorization_capacity
requires finishing an approval or waiting for expiry. Expired rows are pruned
by the existing expiry rule during another start or handoff.
Hosted polling
Platform's public route rate-limits each terminal, then calls authenticated
machine_authorization.handoff_poll with {device_code, machine_id, machine_name, org_id}. Poll takes the original raw device secret, plus the
same engine org ID used at handoff. The function checks the service's grant
and the stored code's exact org before calling the existing domain poll.
It bypasses the engine's public route admission, so many hosted terminals do
not share Platform's proxy-address bucket. Public self-host admission is
unchanged; this is no new public endpoint or credential store.
The result is {authorization: {state, machine_id, token?}}. State is
pending, approved, denied, expired or consumed, projected from the
existing poll outcome. Only approved includes token, once. The approving
member's live membership and machine ownership still gate delivery, and
consumption and issuance commit together. Concurrent polls yield at most one
raw token. A failed mint rolls consumption back. Network loss after delivery
requires a new terminal connection; the token cannot be replayed.
Both functions use handler-managed idempotency. The dispatcher neither caches
nor replays polling credentials; the one-time-result guard also excludes them
from detailed debug captures. A reused request ID polls current durable state.
authorization_unknown requires a fresh connection; machine_identity_mismatch
requires the original machine and consumes nothing. Credential and actor
refusals retain their named code and recovery. Platform maps the result to
its public status, org slug and API URL; an engine function envelope itself
uses the normal function-call transport. Database failures refuse as
authorization_store_unavailable; restore database health before retrying.
The hosted consumer must adopt both explicit-org payloads and prove its real routing against the exact merged engine candidate. Engine source tests do not prove hosted route adoption. Existing hosted routing remains until that consumer cutover; the consumer keeps Members panels until the pinned Team destination is available.