Skip to content

Bridge project scope and Work Center feeds

The Bridge is a local execution boundary. Its reviewed bridge:transport credential authenticates bridge_identity_id, not a selected project. A node may work for more than one HeyAira project only when each project has an explicit active binding.

This contract keeps the account, project and node boundaries separate:

  • project_id is never selected from a Bridge read request to grant access;
  • the Bridge access token identifies the node;
  • a project filter is accepted only after the server verifies an active node-to-project binding;
  • binding requires a project owner/admin with bridge:bindings:write, and the same authenticated subject must also be an owner/admin of the Bridge identity's enrollment project;
  • unbinding requires bridge:bindings:write and an owner/admin role only in the authenticated target project, so its data owner can always revoke the delegation;
  • an unbound or revoked project fails closed with project_not_bound or a not-found response, and no task, memory, receipt or source data is returned.

Persistence and migration

Migration 017_bridge_node_project_bindings.sql adds the explicit binding relation:

(bridge_identity_id, project_id) -> created_at, created_by_subject, source, revoked_at

Existing reviewed bridge_identities rows are backfilled as bridge-enrollment-backfill. New enrollment proofs create the initial binding in the same transaction that activates the identity and issues its token. The older /pair credential remains valid for its original single project through a read-only compatibility fallback; it cannot silently acquire more projects. The migration is additive and does not delete or rewrite project data.

Binding management

The authenticated project is derived from the project token. The request body does not contain a target project, so a caller cannot switch tenants by changing JSON. Creating or restoring a binding is a durable delegation and therefore requires all three checks:

  1. the credential carries bridge:bindings:write;
  2. its subject is an owner or admin of the authenticated target project;
  3. that same subject is an owner or admin of the Bridge identity's original enrollment project.

The third check proves that the caller controls the Bridge identity rather than merely knowing a node UUID. A foreign identity is returned as not found, and a rejected request does not create or restore a binding. Limited OAuth grants such as memory:read cannot manage bindings even when their subject has an administrative project role.

POST /bridge/v1/nodes/{node_id}/projects
Authorization: Bearer <project-owner-or-admin-token-with-bridge:bindings:write>

The response contains the node ID and the bound project metadata. To revoke only that project's access:

DELETE /bridge/v1/nodes/{node_id}/projects
Authorization: Bearer <project-owner-or-admin-token-with-bridge:bindings:write>

Unbinding is intentionally asymmetric: the authenticated subject must administer the target project but does not need to administer the node's enrollment project. Revocation only removes the target project's delegation; requiring node control would let a removed administrator keep a binding that the target project's remaining owners could not revoke.

Here node_id is the database UUID of the reviewed enrollment identity (bridge_identities.id). It is not the legacy /pair node UUID and not the client-chosen textual bridge_id.

The existing POST /bridge/v1/nodes/{node_id}/revoke remains the legacy global node-revocation route for /pair credentials. Modern transport-token revocation continues through POST /bridge/v1/tokens/revoke; project unbinding is independent and revokes only one binding.

Read feeds

Bridge access tokens can read the following bounded, no-store feeds:

Endpoint Purpose
GET /bridge/v1/projects Active project catalog for this node
GET /bridge/v1/tasks Task index with optional project_id, status, limit, and offset
GET /bridge/v1/tasks/{task_id} Full task plus work sessions, only within an active binding
GET /bridge/v1/memory Memory index with optional project, kind and text filters
GET /bridge/v1/memory/{memory_id} Full memory entry, only within an active binding
GET /bridge/v1/receipts Bounded work-session receipt index
GET /bridge/v1/sources Bounded Git document-reference index

Index responses do not include memory bodies. Detail endpoints perform the same binding check as index endpoints. Limits are clamped to 100 entries per request. Feed responses contain references and durable metadata; repository files remain in Git and are read through the existing pinned document access contract.

Client integration boundary

The Bridge client should call the project catalog after authentication, show the user only projects returned by that catalog, and send an explicit project filter for project-scoped Work Center views. A client must treat a 403 project_not_bound or a 404 detail response as an authorization result, not as permission to retry against another project or infer a project from a task.

Session assignment, leases, and server-issued task plans are intentionally not part of this slice. They require a separate session registry and assignment contract. Until then, the client must keep the project ID in its local session state and record it in the normal task/work-receipt flow.