Skip to content

HeyAira Bridge 0.0.1 protocol

The complete phone notification event list is in the notification reference.

Bridge workflow dispatch and execution callbacks were removed in S5 (owner decisions a9f0e60e and 6cab5c63). Coordinators run tasks with scripts/heyaira-run.sh and record results with task_update. Historical workflow data remains readable; no migration or data deletion is part of this change.

Transport and authentication

The Bridge initiates outbound HTTPS requests through the selected Tailscale route. The Mac does not expose an inbound listener. Tailscale provides network reachability only; the Bridge API still authenticates every request.

Pairing is a one-time code created by an operator with:

heyaira-admin create-bridge-pairing \
  --project-id <project-id> \
  --code-file /run/heyaira-secrets/bridge-pairing-code

The Bridge creates a random stable node_id and an Ed25519 public/private key pair. The private key remains in the macOS Keychain. The pairing response contains a short-lived access token and a longer-lived refresh token; the Bridge stores both in the Keychain. PostgreSQL stores only hashes.

Endpoints

POST /bridge/v1/pair

Consumes a one-time pairing code and registers the node. The code is never logged or returned by the server. The request includes the public identity and bounded machine metadata. A successful response returns the node ID and credentials.

An optional device_id (1–256 characters, not blank) names the physical machine and must be distinct from node_id: a stable hardware identifier such as the Mac's IOPlatformUUID, not a value stored in the keychain (a restored keychain carries node_id to new hardware, the hardware identifier does not). Pairing records it as the node's device binding; a later pairing with a fresh owner-issued code may replace it, which is how a Mac restored onto new hardware is re-bound. Omitting it keeps any existing binding.

POST /bridge/v1/token

Consumes a refresh credential and returns a new short-lived access token and the refresh credential's current refresh_expires_at. The node ID in the JSON body must match the authenticated refresh credential. The refresh token itself is never replaced: deployed Bridges keep the token from pairing, so a new one would lock them out (enrolled Bridges use the two-phase rotation below). When a renewal arrives inside the last 7 days of its validity, the server slides the same credential's expiry to 30 days from now, but never beyond 365 days after pairing; after that the owner pairs the Mac again, and Server Beat's bridge.online alerts while it is offline. Until 19.10.2026 the expiry did not slide and a running node lost its credential after 30 days.

POST /bridge/v1/heartbeat

Consumes the access credential and a monotonically increasing sequence number. Duplicate or older sequences are idempotently acknowledged without changing the node state. A newer sequence updates health and sets the node to connected.

A heartbeat may carry the same optional device_id. A node without a binding (paired before bindings existed) is bound by its first heartbeat that carries one, under a row lock. Once bound, a heartbeat with a different device_id is refused with 409 {"error": "device_mismatch"} and audited, without changing the node; the owner re-binds by pairing again with a fresh code.

Additional health metadata from older clients remains compatible. It does not cause the server to dispatch executions. Presence and device binding still follow the heartbeat rules above.

GET /bridge/v1/node

Returns the authenticated node's bounded metadata and one of connected, offline, or revoked.

GET /bridge/v1/nodes

Returns nodes belonging to the project selected by an existing project-scoped HeyAira bearer token. It is an operator/API surface, not a Bridge credential surface. The token must carry tasks:read, otherwise 403 {"error":"insufficient_scope"}.

POST /bridge/v1/nodes/{node_id}/revoke

Revokes the node and all of its active credentials in one transaction. The caller must be a project owner or admin whose credential carries bridge:bindings:write (a project token does; an ordinary OAuth grant does not): otherwise insufficient_scope or project_admin_required (403). The node must pair again to become trusted.

POST /bridge/v1/notifications

Queues one notification event for a node of the caller's project. Because the text reaches the owner's inbox and phone, it has the same requirement as the revoke route above: project owner or admin with bridge:bindings:write.

POST /bridge/v1/nodes/{node_id}/projects

Creates or restores a binding to the project selected by Authorization: Bearer <project-token>. The actor must carry bridge:bindings:write and be an owner/admin of that target project. Control of the enrolled Bridge is proved by either:

  • the same subject being owner/admin of its enrollment project (existing path);
  • X-HeyAira-Bridge-Authorization: Bearer <transport-token> containing a valid, active, unexpired bridge:transport token of that exact identity, with target and enrollment projects in the same account.

The identity must be active. The extra header never grants project authority or substitutes for the actor's required scope. Invalid/missing co-signatures, other identities and cross-account attempts on the alternative path return 404 not_found without creating or restoring a binding.

The path first resolves an exact bridge_identities.id UUID. That match wins over any device bridge_id collision, with no authorization fallback. Otherwise it accepts a bridge_id only when it matches exactly one identity globally (revoked identities count); ambiguous or missing matches return 404. Success is 201 with {"bound":true,"node_id":"<identity-uuid>","project":...}. No target project is accepted in the body. Tokens are never returned.

DELETE at the same path is unchanged: it takes the identity UUID and requires only the target project's owner/admin and bridge:bindings:write, without any co-signature. See Bridge project scope for its cancellation effects and the Mac's project bind --project <slug> OAuth command.

Mobile notifications (Bridge 0.0.4 boundary)

Mobile notifications are a project-scoped server boundary, not an iOS UI implementation. The current server accepts iOS/APNs registrations and queues safe lifecycle events. The provider is disabled by default until a deployment supplies an external secret-management boundary for APNs credentials and device-token retrieval. No APNs private key, team credential, authorization header, or raw device token is stored in PostgreSQL or returned by the API.

All mobile endpoints use the existing project-scoped Authorization: Bearer token, which must carry tasks:read to list devices or notifications or mark them read (otherwise 403 {"error":"insufficient_scope"}); the HeyAira app has always requested it. The token selects the project and its subject; a device can only be read, changed, or revoked by that same project/subject. A token hash is salted with the project ID before persistence, and re-registering with the same device_id replaces the environment/token hash atomically. Reusing a token under another device is rejected.

POST /bridge/v1/mobile/devices

Register or replace a device. The request is accepted only for platform: "ios" and environment: "sandbox" | "production":

{
  "device_id": "optional-existing-device-uuid",
  "platform": "ios",
  "environment": "sandbox",
  "device_token": "synthetic-or-provider-token",
  "preferences": {
    "severities": ["attention", "warning", "error"],
    "event_types": [],
    "quiet_hours_start": "22:00",
    "quiet_hours_end": "07:00",
    "quiet_hours_timezone": "Europe/Warsaw"
  }
}

device_token is write-only at this boundary. The response contains only the device ID, platform, environment, status, preferences, timestamps, and provider: "disabled"; it never contains the token or its hash.

The default preferences deliver important events (attention, warning, and error), accept every event type, and have no quiet hours. event_types can restrict delivery. Quiet hours support overnight windows and an IANA timezone. Heartbeat events are always filtered so a connected client does not create notification spam.

The APNs deployment inputs are mounted and configured separately from the database: HEYAIRA_APNS_KEY_FILE, HEYAIRA_APNS_KEY_ID, HEYAIRA_APNS_TEAM_ID, and HEYAIRA_APNS_TOPIC. Set HEYAIRA_APNS_ENABLED=true only for an explicit provider review. The delivery loop additionally requires HEYAIRA_MOBILE_DELIVERY_ENABLED=true; production delivery requires the separate HEYAIRA_APNS_ALLOW_PRODUCTION=true switch.

GET /bridge/v1/mobile/devices

Lists active and revoked devices owned by the authenticated project subject. It returns the safe registration metadata and preferences only.

PUT /bridge/v1/mobile/devices/{device_id}/preferences

Replaces the validated routing preferences. A revoked device cannot be modified; it must register again.

POST /bridge/v1/mobile/devices/{device_id}/revoke

Revokes the device and prevents future delivery. Revocation is idempotent from the client's perspective: the resulting state is revoked.

GET /bridge/v1/mobile/notifications

The notification list behind the iOS inbox. Same project-scoped bearer as the other mobile endpoints; the caller must own an active registered device, otherwise 403 {"error":"no_registered_device"}.

Query parameters (all optional, invalid values return 400):

name meaning
limit 1–100, default 30
severity repeatable: info, attention, warning, error
event_type repeatable, ^[a-z0-9_]{1,80}$
project_id must equal the token's project, otherwise the list is empty
since, until ISO-8601 timestamps with a timezone
unread true to hide items already read by this owner
cursor opaque next_cursor from the previous page
{
  "items": [{
    "event_id": "uuid", "dedupe_key": "task:…", "event_type": "task_blocked",
    "severity": "error", "title": "Task blocked: <task title>",
    "body": "Blocked. Open the task in HeyAira to see what is needed.",
    "task_id": "uuid", "task_title": "…", "task_status": "blocked",
    "task_next": "handoff next step or null", "task_blocker": "blocker or null",
    "project_id": "uuid", "project_name": "…", "deep_link": "…",
    "created_at": "2026-09-25T10:01:00+00:00", "read": false
  }],
  "next_cursor": null,
  "unread_count": 3
}

A logical event can be stored several times (one copy per Mac that carries it, and the project's own copy when project copies are on, see Who gets a task event); the list collapses those copies by dedupe_key and returns the newest. Pagination is keyset based (created_at, event_id), newest first, so pages neither skip nor repeat rows while new events arrive. task_title, task_status, task_next (the handoff's next step) and task_blocker are read live from the task, so they reflect the current state; the handoff texts are at most 600 characters, keep paragraph breaks, and are null when empty. They appear only in this authenticated list, never in a push payload.

POST /bridge/v1/mobile/notifications/read

Body {"event_ids": ["uuid", …]} (at most 100) or {"all": true}; anything else is 400. Read state is shared per project and keyed by dedupe_key (bridge_mobile_event_reads, reader project): marking any copy read marks the logical event read, and an item read on the iPhone is read in the Mac Bridge app and the other way round. Ids from another project mark nothing. Idempotent. Returns {"marked": n, "unread_count": m} where marked counts newly read events.

GET /bridge/v1/inbox and POST /bridge/v1/inbox/read (Mac Bridge app)

The same list and mark-read contract as the two mobile routes above, for the Mac Bridge app. Authentication is the Bridge node credential (reviewed transport token or legacy node access token) instead of the OAuth actor, and the list covers every project the node is bound to; project_id narrows it to one of them (a project the node is not bound to yields an empty list, a malformed id 400). Read state is the same shared per-project state as on the phone. The node-scoped GET /bridge/v1/notifications feed that drives desktop banners is unchanged.

Work Center reads from the iPhone app

GET /bridge/v1/projects, /tasks, /tasks/{id}, /tasks/{id}/workflow, /memory, /memory/{id}, /receipts and /sources accept either the Bridge node credential (unchanged) or the owner's project token. A project token becomes a single-project principal for its own project and needs the matching read scope: tasks:read (projects, tasks, workflow, receipts), memory:read (memory) or documents:read (Git sources); without it the route answers 401. No write is reachable this way. On /tasks/{id}/workflow a project token gets the compact contract (instructions, prompt, each profile's soul and every skill's content replaced by markers); a Bridge node gets the full contract.

GET /bridge/v1/sources returns only active pinned document sources by default: every commit_sha is a full 40-character hex SHA, and the existing source fields are unchanged. Clients that understand tracking sources can opt in with include_tracking=true; tracking rows also contain tracking_ref (the branch name) and commit_sha: null. Pinned rows keep their existing fields in either mode. The same authorized project_id filter, ordering and limit/offset pagination apply in both modes; default-mode pagination excludes tracking rows before applying the limit and offset. Revoked sources and repositories are excluded in both modes.

Push content

The notification reference lists task status events, workflow alerts, labels, and severities. Notifications are delivered to the node-scoped feed; default preferences do not push task_started.

Task status pushes use the readable form: title "<Label>: <task title>", body "<Project name>: <fixed sentence>". Task descriptions, handoff text and blocker text are never placed in a push payload; only the task title, project name and a fixed sentence travel through Apple. The APNs payload also carries event_id, dedupe_key, event_type, severity, task_id and deep_link so a tap can open the list with that item expanded (match on dedupe_key: the pushed event_id is the copy that carries the push, which the list may not return).

Who gets a task event

Each Mac that serves the project gets a copy of a task event for its own feed: every Mac of the project that is not revoked, awake or not, and a Mac paired in another project that is bound to this one. So a task that stops because the Mac went away still reaches the phone and the inbox, and the Mac reads the event when it is back.

Every active registered phone of the project gets one queued push per logical event (dedupe_key), and no phone of another project does. A revoked phone gets nothing, and revoking a project token revokes its phones. Which copy carries the pushes depends on the server setting HEYAIRA_NOTIFICATIONS_PROJECT_COPIES:

  • On: the event belongs to its project. It is also stored once for the project (node_id null), whether or not any Mac is paired in or bound to that project. That copy is the inbox item and carries the pushes, so a project with no Mac still reaches its phones and inbox.
  • Off (the default): the first Mac copy carries the pushes, as servers before migration 036 did. A project with no Mac gets no event, no push and no inbox item.

The setting only decides what is written. A server reads project copies whichever way it is set, so it can be switched on in a later deploy than the one that brings the code (see Notification project copies).

A bound Mac only carries the other project's event. GET /bridge/v1/notifications is read with the node's own pairing credential and returns events of the node's own project only, whether the binding is active or revoked.

Phones and projects

A phone registration belongs to one project and one subject. The bearer token selects both, and an OAuth subject is always token:<project token id>, so it names a credential of one project, not a person across projects. The device token hash is salted with the project id, so the same physical phone registered in two projects is two unrelated rows. Since an event belongs to exactly one project, such a phone still gets each event once (on that project's row), and nothing for a project it is not registered in. The iOS app holds one OAuth grant and one device id at a time, so today it is registered in one project; another project's events reach it only after the app is connected to that project too. Delivering every project of one person to one phone registration needs a person identity across projects, which the server does not have yet.

GET /bridge/v1/notifications paging

Query after (a cursor) and limit (1–100, default 20). Events written in one transaction share created_at, so the feed's position is (created_at, event_id). next_cursor is opaque to the Bridge, which stores it and sends it back as after. A server accepts both forms below as after whichever way HEYAIRA_NOTIFICATIONS_PROJECT_COPIES is set. The setting only decides which form it hands out:

  • On: when a page ends where an instant ends, next_cursor is that event's timestamp, the form an installed Bridge already has stored. A timestamp means every event of that instant has been read. When a page ends inside one instant, next_cursor is an opaque cursor that also carries the last event id, so the next page starts with the following event of the same instant. Any number of events in one instant is paged without stalling or skipping. An empty page returns the cursor it was given.
  • Off (the default): next_cursor is a bare timestamp, which a server from before migration 036 can read. A page therefore never ends inside one instant: it may return more than limit events, up to 500, to finish the instant. With more than 500 events in one instant the rest of that instant is still skipped, exactly as before. An empty page returns after as a timestamp. The one exception is an after that already carries an event id (handed out while the setting was on). The server then first reads the rest of that instant by event id, at most limit events per page. It returns id-carrying cursors until the instant is read, then that instant's bare timestamp, so nothing is skipped when the setting is turned off.

An event's created_at is the start of the transaction that writes it, not its commit, so an event can become visible after the feed has handed out a later instant, or the same one. A cursor past that instant would never return it. The feed therefore hands out only instants that have settled: earlier than the start of the oldest transaction still open on the database, and at least one second old. A newer event comes with a later poll, about a second after it commits. Whichever way the setting is set, an after that carries an event id inside an instant that has not settled yet is returned unchanged with an empty page. The cursor forms do not change, so a server without this rule reads every cursor this one hands out. One limit: a transaction open for more than five minutes stops holding the feed back, so that a forgotten open transaction cannot stall it. An event such a transaction writes and commits after the feed has moved past its start is still skipped.

A missing after starts at the current time; a malformed one is 400 invalid_cursor.

Delivery and retry contract

Task lifecycle changes (in_progress, blocked, done) already create safe, deduplicated events. For each newly-created event, the active mobile devices of its project receive one queued delivery row on the copy that carries the pushes, with a unique (event_id, device_id) key. A future delivery worker applies preferences and calls the provider adapter. The adapter returns sent, retry, filtered, or failed; a delivery gets at most five attempts in all, the first one and four retries, so retryable failures wait 5, 10, 20 and 40 seconds (exponential backoff, each delay capped at 300 seconds); a retryable failure on the fifth attempt ends the delivery failed. Provider errors are stored as bounded codes, never raw responses.

The repository includes a fake provider for synthetic tests and a disabled provider for the default deployment. MobileDeliveryWorker.poll_once claims at most a bounded batch using short-lived database leases, reads raw tokens only through the injected MobileTokenVault, and finalizes rows only when the lease still belongs to that worker. An expired lease is reclaimable after a crash; the worker has no inbound listener or unbounded loop. The delivery rows therefore move through queued → sent, filtered, retry, or failed, with retry returning to the due queue and failed/filtered being terminal for that attempt.

The production adapter boundary is ExternalEncryptedMobileTokenVault plus ExternalAPNsProvider. The vault adapter delegates put, get, and revoke to an injected encrypted secret store; the store owns encryption at rest, access control, rotation, and audit logging. The adapter does not cache tokens or write them to PostgreSQL. The APNs adapter uses a bounded HTTP/2 JWT transport, reads the .p8 credential only from the mounted secrets path, is disabled by default, refuses production delivery unless explicitly enabled, and maps provider failures to a small allowlist of safe retry/error codes. FakeAPNsProvider and test-only fake vault stores exercise the contracts without Apple network access.

Enabling production APNs remains a separate operational step requiring an encrypted external token vault, APNs key rotation, a delivery supervisor, and an explicit production configuration review. The lease migration does not move raw device tokens into PostgreSQL.

Delivery supervisor

MobileDeliverySupervisor is the deployment-owned scheduler around MobileDeliveryWorker.poll_once. It acquires one database connection per bounded poll, waits for the configured interval after success, and applies bounded exponential backoff after failures. It never logs exception text, tokens, or payloads. request_stop() finishes the current poll and exits; request_restart() exits with a restart result so a process manager can start one replacement run. A second concurrent run() on the same supervisor is rejected, so restart orchestration cannot accidentally create two polling loops. The worker's short lease and SKIP LOCKED claim remain the duplicate delivery guard if a process crashes between polls.

The server can construct the supervisor when HEYAIRA_MOBILE_DELIVERY_ENABLED=true, a configured OpenBao vault, and an enabled APNs provider are all present. It remains disabled by default. A safe deployment must also set HEYAIRA_APNS_ALLOW_PRODUCTION=false for sandbox validation and only change it after a real-device acceptance.

0.0.1 acceptance criteria

  • an unpaired node cannot register;
  • pairing is one-time and expires;
  • node identity survives a Bridge restart;
  • heartbeats move the node to connected;
  • missing heartbeats are reported as offline;
  • duplicate heartbeat sequences are safe;
  • revocation blocks further heartbeats;
  • reset removes local identity and credentials;
  • secrets are absent from logs, API responses other than the initial pairing response, and database rows.

Recoverable Bridge access-token rotation

This document is the normative lifecycle contract for the project-fenced Bridge access credential. Bridge enrollment is separate from MCP OAuth and a Bridge token never grants access to another project.

Token states

Each stored token is pending, active, or revoked.

  • Exactly one token per Bridge identity may authorize normal Bridge work.
  • A pending token may call only its own rotation confirm or abort route.
  • A revoked or expired token cannot authorize work or reveal a successor.
  • PostgreSQL stores token hashes only. Raw tokens appear only in the original no-store HTTP response and the Bridge Keychain.

Two-phase rotation

Prepare

POST /bridge/v1/tokens/rotate/prepare is authenticated by active predecessor T0 and requires Idempotency-Key. It creates one short-lived pending successor T1 and a rotation_id; T0 remains active. Exact replay while the rotation is pending returns the same T1. A conflicting prepare returns bridge_rotation_in_progress.

The Bridge must store one versioned Keychain journal containing T0, T1, rotation_id, prepare and confirm idempotency keys, and the current phase before sending confirmation.

Confirm

POST /bridge/v1/tokens/rotate/confirm is authenticated by T1 and carries the matching rotation_id. The transaction verifies that the identity and T0 are still active, activates T1, gives it a fresh normal token lifetime, revokes T0 and marks the rotation confirmed. Exact replay with active T1 returns the same non-secret status metadata.

Abort and expiry

POST /bridge/v1/tokens/rotate/abort is authenticated by pending T1. Abort or pending expiry revokes T1 but leaves T0 active. A fresh prepare may then begin with a new idempotency key. Emergency revocation of T0 also aborts every pending successor so a delayed confirmation cannot restore authority.

Failure recovery

Failure point Durable client state Recovery
Prepare response lost T0 + prepare key Replay prepare exactly
T1 cannot be saved T0 remains active Do not confirm; abort or allow expiry
Process restarts after saving T1 T0 + T1 + journal Resume confirm with T1
Confirm response lost T1 + confirm key Replay confirm exactly
Pending rotation expires T0 + stale journal Discard T1/journal and prepare again
Suspected compromise Operator recovery authority Use emergency revoke, not routine rotation

The continuously running Bridge rotates before the active credential enters its final five minutes. Network calls are bounded and retry backoff is capped, so the local lifecycle lock cannot be held indefinitely. If an older client has already allowed the credential to expire, the supported recovery is explicit and operator-mediated:

  1. run heyaira-admin recover-bridge-token for the existing project and Bridge identity, writing the one-time JSON credential to a new 0600 file;
  2. transfer that file through the authorized administrative channel;
  3. pipe it to heyaira-bridge token recover on the enrolled Mac;
  4. the client validates the replacement through GET /bridge/v1/tokens/status before replacing Keychain state.

Recovery keeps the existing active identity and project binding. The server atomically aborts pending successors, revokes all predecessor tokens and installs exactly one active replacement. It is not re-enrollment, does not accept direct database edits, and refuses a client journal that contains real successor recovery evidence. The server also rejects recovery while any valid active token remains; healthy credentials must use routine rotation instead.

The retired one-phase route /bridge/v1/tokens/rotate always returns bridge_rotation_protocol_upgrade_required without changing state. This prevents an old client from treating a pending successor as active.

Persistence and rollout

Migration 016_bridge_token_rotation.sql adds explicit token state and the rotation ledger. Existing unrevoked tokens migrate to active; revoked tokens migrate to revoked. A partial unique index permits at most one pending rotation per Bridge identity. A compatibility trigger maps a legacy revoked_at-only update to state = 'revoked', so an automatic application rollback to the immediately preceding release cannot silently lose emergency revocation after the additive migration has committed. The same trigger rejects that release's one-phase rotation pattern when it detects a second active successor, so rollback cannot reintroduce the lost-response lockout.

Roll back the application before rolling back this additive schema. Do not drop the new table or columns while a prepare/confirm-capable client is in use. Production rollout requires an independent PASS, a database backup, the complete additive Bridge migration chain, server deployment, then the matching Bridge release. The server and client must not be activated independently.

Interactive task claims

The interactive session claim protocol remains available: create a claim with POST /bridge/v1/claims, read it with GET /bridge/v1/claims/{claim_id} or GET /bridge/v1/tasks/{task_id}/claim, report through POST /bridge/v1/claims/{claim_id}/report, and release through POST /bridge/v1/claims/{claim_id}/release. These use the reviewed node token, active project binding and existing claim validation rules. They do not create Bridge workflow executions.

Interactive PR CI follow-up

The server recovery loop keeps interactive task CI follow-up, unattended-move alerts, and expired-execution settlement (for one more release). It no longer runs Bridge workflow alerts, correction/retry offers, publication rechecks, or workflow CI polling. Signed GitHub run deliveries still update stored repository CI and wake interactive watches; runs and closed pull requests no longer advance Bridge workflows. No schema or stored data is removed.

POST /bridge/v1/tasks/{task_id}/ci-follow uses the same authenticated Bridge node and active node-project binding as interactive claims. A claim secret is not required; organization assignments do not grant access. The request is strict:

{
  "expected_version": 4,
  "pr_number": 7,
  "head_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "repository": "example/synthetic",
  "write_context": {
    "resource": "https://mcp.heyaira.eu/p/example/mcp",
    "instance_id": "expected-instance",
    "project_id": "00000000-0000-4000-8000-000000000002"
  }
}

head_sha is required: omission or null returns head_sha_required; it must be this session's local worktree HEAD and equal the open PR's current head. Optional repository is GitHub owner/name and must match the task's selected repository, case-insensitively (repository_mismatch). Bridge obtains these facts from git rev-parse HEAD and git remote get-url origin before sending; an explicit --sha must equal HEAD, and --repo overrides the repository it sends. A missing Git checkout/origin fails locally without a request.

write_context is optional for a Bridge node, which cannot learn the instance identity: the node token and its project binding are the authority, as for claims. When sent, its resource must be the task project's MCP address (or the legacy MCP resource), with matching instance/project. The task's repository or its project's sole active repository binding is used. With multiple active bindings and no task repository, the server returns repository_required: set repository="owner/name" using task_create or task_update. It never searches other project repositories for a matching PR number or infers the caller's SHA from GitHub. The result is HTTP 200 with task and organization, including organization.ci_follow_up. A stale version or workflow task returns HTTP 409 (version_conflict, stage_managed_by_workflow). An unbound node receives 404 for both methods. Invalid stages/inputs/context, missing links (repository_not_linked) and unavailable GitHub App access (github_not_connected) return 400. Execution in progress or verification enters ci; an existing CI watch may be replaced. See settlement rules.

Release coordination: install the new Bridge and deploy the server together through DevOps. Bridge b2705e4 without --sha receives head_sha_required after the server upgrade (safe refusal, not backward compatible). No migration is required.

GET /bridge/v1/tasks/{task_id}/ci-follow is read-only, requires the same node binding, and returns task_id, version, ci_follow_up (null if never followed) and organization. Both methods send Cache-Control: no-store. A watch includes id, task_id, project_id, repository_id, pr_number, head_sha, state (watching|passed|failed|unfinished|cancelled), runs_summary, polls, poll_after, created_by, created_at, settled_at. Each run contains only id, workflow_id, name, status, conclusion, html_url, updated_at. Clients can wait until the watch is terminal; replacing a watch cancels the old one and GET reports the latest. These endpoints do not start or wake a session.