# Project queries and integration events, version 1 Sources: `proto/pharos/project/v1/requests.proto` (the envelope), `queries.proto` (directory and canonical query inputs) and `replies.proto` (their typed results). Run `pnpm generate:contracts` after edits. Generated Go bindings live in Beto's outer contract package and the API's messaging adaptor. Metallo receives generated Python query/event bindings. Each service compiles schemas into its own package; consumer wiring remains explicit and is deferred in this slice. `GetProjects` declares its owner, public visibility and `Projects` success response through the `pharos.contracts.v1.contract` schema option. These build-time annotations preserve the existing wire fields. Generation validates the association against `ProjectReply.payload`, including its `RequestError` alternative. The retained directory operation is `GetProjects`. A `ProjectRequest` has contract version 1, context `project`, a UUID `request_id` and `query.get_projects`. The query has no parameters. A `ProjectReply` repeats the version, context and request identity, with either `projects.items` or `error.code`. ## Canonical owner boundary Canonical Project IDs are independent UUIDs. Provider IDs remain opaque bindings. These operations preserve the existing directory fields and wire numbers: | Query | Success and missing-resource semantics | | --- | --- | | `ResolveProjectProviderBinding` | Exact provider/instance/external ID to Project ID and revision; `unresolved` until a complete canonical snapshot exists. | | `GetProjectIntegrationSnapshot` | Complete canonical Project plus separate source status; `not_found` for an absent canonical Project. | | `CheckProjectCapability` | Correlated Party/Project/capability, allowed/denied, Project revision and Party eligibility evidence; `stale_authority` when source authority is pending, failed or expired. | | `ListProjectIntegrationSnapshots` | Immutable, paginated inventory for one authorised provider/instance/scope. The first request opens a 24-hour checkpoint; subsequent pages supply its ID and returned cursor. | | `GetProjectMembers` | Current actor’s bounded Project membership, canonical Party IDs/roles, nullable trusted AAI names with independent revisions, membership completeness and diagnostic unresolved-member count; requires fresh `project.view` authority. | | `ListProjectInvitations` | Complete bounded pending, unexpired Project invitations; requires current `project.access.manage` before and after the provider read. | | `ListMyProjects` | Current actor's Projects with `project.view`, including UUID, revision, name, organisation and lifecycle. An account without accessible Projects returns an empty page. | The published capability matrix is: | Capability | Decision | `developer` | `project-administrator` | | --- | --- | --- | --- | | `project.view` | Read basic Project information. | allow | allow | | `runs.view` | List Runs and read operational state, initiator and resource use. | allow | allow | | `runs.output.view` | Read logs and published results, which may contain repository content. | allow | allow | | `runs.launch` | Accept a new Run. | allow | allow | | `runs.retry` | Request a manual retry. | allow | allow | | `runs.cancel` | Request cancellation as the current person. | allow | allow | | `workloads.view` | Read Workloads and immutable revisions. | allow | allow | | `workloads.manage` | Create, revise or retire a Workload. | allow | allow | | `pipelines.view` | Read a Pipeline and operational progress. | allow | allow | | `pipelines.start` | Accept a new Pipeline. | allow | allow | | `pipelines.cancel` | Request Pipeline cancellation as the current person. | allow | allow | | `repositories.delete` | Request deletion of a Project repository through Source Control. | deny | allow | | `project.access.manage` | Administer Project access. | deny | allow | `runs.view` grants operational visibility without granting repository, private workspace, log or result contents. Source Control and Site Runtime retain their own repository and workspace rules. `project.read`, `project.execute` and `project.manage` remain accepted transition aliases for #794's current callers. They are removed after the three Metallo consumers use the explicit capabilities, `metallo-requests` has an enrolled Beto caller and the real broker lane qualifies each caller. New call sites must use the explicit matrix. Roles are `developer` and `project-administrator`. Each membership row retains its qualified Party binding ID/revision and opaque external account ID. Several valid rows for the same Party contribute their roles; unresolved members have no canonical membership row. Unsupported roles grant nothing. `pending_member_count` and `governance_issues` expose incomplete identity resolution and missing managers. Source snapshots are complete and limited to 256 members. Exceeding the limit rejects the refresh and preserves prior state; there is no truncation. Integration queries require an authenticated service principal and an explicitly configured provider/instance/source scope. Actor-bearing requests cannot use the integration snapshot, inventory or binding-resolution queries. Invalid input, forbidden scope, missing authority and temporary failures retain distinct outcomes. Authority failure is never an allowed capability decision. A positive decision requires fresh complete source evidence, a currently established exact binding and current Party eligibility. It is evidence for the current operation, not a reusable permission token. `ListMyProjects` accepts only the API's authenticated Party actor. The Party UUID and authentication version come from broker-validated actor headers; the payload contains only an optional `after_project_id` and `page_size` (default 25, maximum 100). This actor may also check its own capabilities. Every returned Project passes the current `project.view` decision, including exact binding, eligibility, source freshness and revision checks. Pending, failed or stale candidate authority fails the page; it never becomes an empty membership result. The handler rechecks Party eligibility and source expiry before returning the page. Pages sort by canonical UUID and return `next_after_project_id` only when another authorised Project exists. The cursor is the last returned UUID. Each page reads current authority; there is no frozen inventory checkpoint. Restart the listing to include membership changes before the cursor. The API exposes this query as `GET /api/v1/me/projects`, with `cursor`, `page_size`, `items` and nullable `next_cursor`, using a canonical Party browser session. The web page loads it through the generated OpenAPI infinite-query client, with explicit refresh and load-more actions. Technical operator sessions have no Party membership. `ProjectSourceStatus` binds its acquisition ID, sequence, scope and material snapshot fingerprint to the returned Project. `last_complete_read_at` records a successful complete read, including unchanged reads. `valid_until` uses the owner's configured freshness budget; retries/replay preserve the original time. `authority_revision` and `changed_at` order and date owner authority transitions independently of material Project revisions. State is `pending`, `ready`, `failed` or `stale`. An admitted native notice within the retry budget invalidates the source until its corresponding complete read is committed. Routine scans preserve the last complete evidence and its original expiry while another read is in progress. They cannot clear a failure or a pending notice, and scheduling alone emits no renewal. Exhausted sources retain failed authority through notices and periodic five-minute probes. Operator resume preserves the failure while making work due; subsequent admitted notices can mark it pending. Only a successful complete read can restore ready authority. | Public event | Meaning | | --- | --- | | `project.ProjectChanged` | Complete material canonical snapshot, including metadata, provider binding and memberships. Payload revision equals the envelope's Project revision. | | `project.ProjectAccessChanged` | Project ID/revision and affected Parties, including removed Parties, for access-specific reactions. | | `project.ProjectAuthorityChanged` | Independently revisioned source authority. Pending/failed invalidates; ready records one complete read and its absolute validity, including an unchanged read. | | `project.ProjectSynchronisationCheckpointPublished` | Announces an owner-materialised inventory checkpoint and its exact record count and manifest hash. It is advisory rather than a queue barrier. | The material and access events can share one Project revision; receipts must distinguish event and reaction identity. They carry no source freshness assertion; the separate authority event does. Consumers use the owner query during the transition and reconcile gaps through its complete snapshot. Bootstrap must bind queues before the owner read. Replay re-emits the retained material snapshot with its original revision and event time; it cannot renew source authority. `ProjectAccessObserved` remains the historical provider observation contract from the source branch and is not published here. `ProjectAuthorityChanged.authority_revision` orders authority transitions for one Project. A ready fact carries the last complete acquisition, source sequence, Project revision, snapshot fingerprint, `last_complete_read_at` and `valid_until`. Pending and failed facts retain the previous complete evidence for diagnosis but deny new access-sensitive decisions immediately. Consumers ignore older revisions and treat equal revisions with different authority content as corrupt. Redelivery retains every timestamp and cannot extend validity. ## Authority delivery and retention Each successful complete read emits one compact `ProjectAuthorityChanged` renewal. It carries the existing evidence fields and no membership list. Routine scheduling adds no pending event. Admitted notices within the retry budget, first or changed failures and material access changes keep their immediate durable publication. Repeated identical failures retain their authority revision and original failure time without another event. The 125-second default and 305-second maximum revocation bounds remain unchanged. Authority consumers persist the latest revision, a fingerprint of its canonical authority content and the projected state atomically, then acknowledge delivery. Older revisions are ignored; equal revisions require matching content. This current record provides durable deduplication for the state-only renewal stream without a permanent receipt row per message. Material access reactions, commands and other external effects retain their own receipts and idempotency rules. The [retention declaration](../../events/retention.json) gives authority subscriber queues a 305-second message TTL. Broker administration applies an exact queue policy, including to existing queues, without deleting or redeclaring them with new arguments. Quorum messages already queued retain the expiry assigned on arrival; the policy bounds subsequent publications. Existing backlog is drained by the consumer's subscription-first bootstrap and revision checks. Material, access-change, retry and dead-letter queues keep their existing policies. Mandatory publication and publisher confirms still apply. Authority events are complete, independently revisioned evidence. After the maximum validity window, an old renewal cannot authorise a decision. Consumers must check `valid_until` on every decision, including when the queue is empty or delivery is interrupted. Expiry of an old invalidation cannot restore access: all ready evidence preceding it has also expired by then. A restarted, delayed or newly activated consumer obtains the complete owner snapshot/checkpoint while consuming and merges by revision before enabling access. Queue emptiness and a broker acknowledgement never prove that its view is current. The owner keeps successfully published authority outbox payloads and dispatches for 24 hours, then deletes them in bounded batches. Unconfirmed deliveries and live leases are preserved. Current authority and command replay records remain available independently of this delivery buffer. Checkpoint retention and outbox retention do not extend permission validity. Public payloads contain no credentials, identity claims or proof documents. Private observation facts remain under Beto's context-local proto and catalogue. Event routing uses `integration.` on `pharos.events`. Execution Core, Pipeline Coordination and Workload Portfolio each have separate durable queues, retry/dead-letter paths and a caller credential that can query Project and Party Governance. Their projection implementations remain consumer-owned. ## Initial load, checkpoints and repair Bind and begin consuming every declared Project and Party queue before opening a Project checkpoint. Read every Project page with the same checkpoint ID, then collect the Party IDs in the returned memberships and query the Party checkpoint at that handover. Keep consuming while both reads run. The retained `GetProjects` directory contains provider numeric IDs and cannot initialise a canonical view. Checkpoint pages are materialised from one repeatable-read owner transaction. Concurrent owner commits therefore appear wholly before or after a checkpoint; they never move records between its pages. Pages sort by canonical Project UUID. An expired or unknown checkpoint must be restarted from the first page. The owner commits the checkpoint rows and exact checkpoint-event bytes together, then a separate durable relay publishes the marker. The Project item hash is SHA-256 of deterministic `CanonicalProject` Protobuf bytes. Source freshness is excluded because Waldur acquisition validity remains Project-owned query evidence. The checkpoint manifest iterates sorted records and hashes each Project ID, revision, lifecycle and item hash, with a zero byte after each value. A consumer compares the complete ID/revision/item-hash set and the manifest before recording the checkpoint sequence as applied. Counts are only a diagnostic. The marker can arrive before older facts because each event has its own queue, broker reconnects redeliver independently and Project material/access events can share a revision. Consumers keep separate receipts by event name and message ID, apply state by aggregate revision, and treat equal-revision material and access events as distinct work. A marker never proves completion on its own. State that is newer than the checkpoint also prevents a direct manifest match; reconcile the checkpoint into a staging comparison, retain newer local revisions, and repeat with a later checkpoint. This rule gives the same result across reconnects and multiple streams. A daily comparison opens a new checkpoint, reconstructs the exact manifest from the consumer view, and repairs missing, different and extra records from the pages. Rebuilding after local data loss follows the same subscription-first sequence. Consumer scheduling, staging and repair transactions belong to the receiving context. Authority bootstrap uses the same subscription-first rule. Apply each checkpoint item's `source_status` by `authority_revision` while material state is staged, continue consuming authority events, then require every retained Project to have a current, non-expired ready authority revision before enabling local access decisions. A restart repeats this merge. An incomplete rebuild, unknown gap, pending/failed state or expired `valid_until` is unavailable and defers access-sensitive work. Material agreement alone never marks authority healthy. The current source policy scans every 30 seconds, with the scan interval required to be no more than half the freshness budget. One acquisition has a 25-second deadline inside a 30-second lease. Ready evidence is valid for two minutes by default and configuration is capped at five minutes. Delivery lag is measured from `changed_at`; consumer apply lag is measured from receipt to its committed authority revision. Both share the remaining absolute validity window. If either does not finish by `valid_until`, the consumer denies. With the accepted five-second source clock skew, the default hard removal bound is 125 seconds from a change immediately after the last complete read; the configured ceiling is 305 seconds. A delivered pending or failed event shortens that bound. The 24-hour checkpoint retention does not extend permission validity. ## Current decisions and accepted work Every person-initiated read, output read, launch, manual retry or cancellation uses that person's current explicit capability. Authorisation is checked before receipt replay. Membership removal, Party disablement and binding retirement remove those operations from that person. For an ordinary departure, suspend the person's access to that Project immediately. Personal reads, control, new launches and interactive Site access to that Project stop; other authorised members continue to manage its accepted work using their own permissions. Final withdrawal of that Project's native access and binding waits until that work finishes, or an authorised cancellation and its cleanup finish. The Site owner also preserves any account still needed by other authorised Projects. Ordinary departure does not itself cancel accepted work. Project-scoped suspension needs Beto-owned commands and published integration facts, with consumers applying the denial within the agreed revocation bound. This owner workflow remains to be implemented. The current source-managed Project aggregate restores only `active`; its lifecycle and Party-wide disablement are separate from the required member suspension within one Project. A successful launch or Pipeline-start decision supplies the Project revision, management generation, source authority revision and validity used at acceptance. The accepting context records that evidence with the exact work identity, immutable Workload or definition revisions, resolved inputs, environment, accepted deadline and initiating Party. This is continuation authority for the accepted work. It is evidence rather than a bearer credential and cannot approve a new or changed definition. Metallo configures the maximum lifetime for accepting further execution under that record; the initial deployment value is 14 days. Save an absolute deadline at acceptance, shortened by an earlier requested or Site limit. Configuration changes affect new acceptances only. A Pipeline and all its declared steps share that original deadline; replay, renewal and automatic retries cannot extend it. Expiry prevents new native dispatch, later Pipeline steps and automatic retries. It does not itself cancel an already-running native job. Native wall-time limits, Project closure and authorised cancellation retain their own rules. Observation, stage-out of the running work, cancellation and cleanup keep narrowly scoped operational authority until their obligations finish. Account retention remains required until those obligations are settled; reaching the dispatch deadline is not a release event. #766 owns configuration and enforcement, with #771 proving these boundaries before activation. Automatic retries of an accepted Run and steps declared by an accepted Pipeline continue under that bounded record after the initiator leaves or is disabled. Accepted-but-queued work follows its saved deadline. A manual retry, added step, changed definition or changed immutable input is a new decision. Project closure or a changed management generation prevents further dispatch and starts durable cancellation and cleanup. An explicit security cancellation does the same for queued and running work. Normal cancellation records the current requester, then continues under its accepted cancellation authority if that requester later loses access. Continuation credentials and native authority are scoped to the accepted Run or Attempt, exact repositories and Site bindings, and its execution and cleanup deadline. They operate independently of the initiator's current membership, interactive login and personal credentials. A Site may retain the exact native account and binding recorded for that work under #765, fenced by its Project, Site and account/binding generation and the accepted-work evidence and deadline. This includes a per-person HPC account identified by `site_username`; the platform authority to operate it remains scoped to the accepted Run and Project. Retention grants no personal access, new launch or changed-work permission. The Site owner must preserve that restricted operational authority until completion or authorised cancellation and cleanup, then complete the deferred withdrawal for that Project. Source Control (#762), Site account delivery (#765), Execution Core (#766) and input resolution (#767) must qualify this boundary before production workers are enabled. Operator observation, security cancellation and cleanup use a separate audited operator authority; member capabilities do not confer it. Metallo's [Run access policy](../../../services/metallo/docs/epics/instantiate-and-run/run-access.md) describes the Site isolation and operational checks required for activation. Decision-time `CheckProjectCapability` calls remain required until each consumer applies the authority stream, proves actual material, Party and binding rows against owner checkpoints, handles retirement and disablement, fails closed on restart and expiry, and passes the real Beto/PostgreSQL/RabbitMQ removal lane. Separate queues provide no shared ordering barrier. Execution Core's worker and Pipeline Coordination's coordinator currently have callers. `metallo-requests` still lacks one and must refuse rather than use a stand-in. Workload Portfolio's caller is in the `metallo` process. The source protocol is adapted from ikons' implementation at [`3636595ff`](https://github.com/pharos-ai-factory/pharos-platform/commit/3636595ff693befcdf0bb12ba6482be865a3e41b). The [context guide](../../../services/beto/contexts/project/README.md) describes ownership, recovery and qualification. Frozen canonical fixtures in `testdata/v1.json` and the retained directory fixtures independently check wire compatibility through generated Go/Python bindings. ## Broker delivery | Boundary | Value | | --- | --- | | Request exchange | `pharos.requests` (durable topic) | | Request routing key | `request.project.query` | | Owner queue | `ref.project.queries` (durable quorum, 15-second TTL) | | Quarantine | `ref.project.queries.dead` | | Reply exchange and key | `pharos.replies`, `reply.project.` | | Caller queue | `ref.api.project.replies.` (exclusive classic, auto-delete, 30-second TTL) | | Request metadata | Persistent, `application/x-protobuf`, type `query`, broker-validated `UserId`, `ReplyTo` set to the caller's instance reply routing key | | Reply metadata | Persistent, `application/x-protobuf`, type `reply`, app ID `project` | | Correlation | AMQP message ID and correlation ID both equal `request_id` | The broker-validated publisher identity, configured caller policy and scoped permissions establish the trusted caller boundary. `AppId` supplies no authentication. Beto publishes mandatory replies with confirms and acknowledges the request afterwards. A failed publication closes the consumer, leaving the request unacknowledged for redelivery. Quorum delivery limits bound repeated failures. Malformed bytes, unknown query variants, wrong owners/versions and mismatched identities are transferred to quarantine with confirmation before acknowledgement. Provider failures produce a typed `temporarily_unavailable` reply; callers can issue a fresh read. Requests have no authoritative side effects or command receipts. Replies preserve the legacy directory fields as typed Protobuf. The API's HTTP adaptor returns a bare JSON array with numeric IDs and snake_case keys, mapping absent descriptions/avatars to JSON null. Protobuf JSON rendering is a different format, including its quoted int64 convention. The legacy endpoint also passes through provider-specific fields beyond the stable directory view; these are outside this typed initial contract. Beto receives its own `PROJECT_DATABASE_URL_FILE`, `PROJECT_BROKER_URL_FILE` and operational API key. Enrolled source workers receive separate restricted provider bindings; administrator credentials remain outside the runtime. `pnpm dev:up` and the context administration command provision the topology with host-side administrator credentials. Beto runtime credentials cannot declare queues, consume caller reply queues, or publish another context's replies. The API translates `GET /api/v1/projects` into this query. It receives only `API_BROKER_URL_FILE` for `pharos_api`, which may publish `request.project.query` and create, bind and consume its connection reply queues. The generated broker catalogue scopes queue authority to `ref.api.project.replies.` and reply bindings to `reply.project.`. It grants no request-queue administration or reply publication to the API. The Argo dev-box overlay provisions the same catalogue in `pharos-broker-provision`, using the administration image after RabbitMQ becomes ready and before application startup. Its projected Secret contains the administrator password and declared runtime bindings. Only that Job can reach RabbitMQ's management port through the administration NetworkPolicies. Repeated runs preserve runtime passwords. Host and Argo provisioning read the generated `runtime_bindings` for every declared Beto owner and caller; Project contributes `PROJECT_BROKER_URL` and `API_BROKER_URL`. Each API broker connection creates a fresh UUID and exclusive queues before it publishes requests. Beto validates `ReplyTo` against the requested owner's route and UUIDv4 format. Separate queues prevent overlapping Pods from consuming each other's replies. Reconnecting removes the old queues and releases pending callers as unavailable; callers repeat uncertain reads with fresh request IDs. An absent instance queue is unroutable, even while another instance remains connected. The retained shared reply queue remains provisioned for explicit administrative cleanup, but authenticated runtime requests require instance-specific `ReplyTo`. The API foundation on this branch already sends broker-validated `UserId` and instance reply routes. Apply the scoped permissions and use this API version before enabling the canonical Beto composition. The [RabbitMQ queue lifecycle](https://www.rabbitmq.com/docs/queues) defines why connection-owned queues are exclusive classic queues. `python3 scripts/beto_broker_integration.py` creates a disposable RabbitMQ container and runs the native Beto process with restricted credentials. It checks concurrent API connections, reconnect, exclusive ownership, scoped permissions, foreign-address quarantine and unroutable-reply recovery. `DOCKER_CONTEXT` selects the local engine without changing the user's default context. Beto retains private `GET /healthz` and bearer-authenticated `GET /diagnostics`. Project queries are available through RabbitMQ only. ## Registration and generation [`broker.json`](broker.json) declares the query queue family and its API and context-specific Metallo callers. `metallo-requests` uses a dedicated query-only principal and reply family under the enrolled `execution_core` subject for its co-hosted Run and Workload decisions. Its declaration sets `consume_events: false`, so it cannot take the execution worker's view deliveries. The [Beto request catalogue](../../beto/messaging/catalogue.json) declares this Protobuf schema for the API. Run `pnpm generate:beto` after changing it or this Protobuf schema; use `pnpm check:contracts:beto` to detect generated drift. The generated broker catalogue is shared by host provisioning and Beto's topology executable. A new request family requires explicit transport support and authority tests before registration. Query handlers must cover every declared query before startup. The same compiled descriptors generate plain input/response models under Beto's `contracts/requests/generated/model/project`, identifiers under `generated/names`, and owner codecs and typed query registrations under `contexts/project/adaptors/messaging/generated`. The application uses the plain contracts; Protobuf runtime dependencies remain in the outer packages. Handwritten read-model aliases preserve the existing application names and legacy JSON keys. Response encoders require exactly one success or error alternative, copy optional values and preserve absence. Signed and unsigned 64-bit values must fit the exact JavaScript integer range; signed values range from `-9007199254740991` to `9007199254740991`. Invalid output becomes a redacted retryable reply. Current canonical requests require explicit scalar fields; the boundary enforces these numeric limits for query inputs as well. Generation rejects missing or foreign contract metadata, invalid reply associations, recursive responses and unsupported field shapes. It supports scalar inputs and scalar, optional, repeated and nested response fields. Maps, enums and user oneofs need an explicit generator extension. Drift checks cover both generated models and owner adaptors, pruning obsolete generated files while preserving handwritten owner code. Historical request/reply fixtures verify wire compatibility separately. ## Browser member and invitation reads The [HTTP integration guide](../http_api/README.md) specifies nullable names, independent revisions, completeness, errors and generated client usage. `GetProjectMemberDetails` enriches only already qualified membership bindings through Party Governance's narrow `GetBoundPartyAssertedName` owner query. It preserves all existing Project and Party identity fields. `pending_member_count` describes unresolved bindings and never represents invitations. The established `GetProjectMembers` query and its `ProjectMembers`/`ProjectMember` replies retain their exact wire fields and dependencies. Existing API instances reject unknown reply fields, so names and completeness use the separate `ProjectMemberDetailsCollection`/`ProjectMemberDetails` messages. Deploying the new owner first therefore preserves the existing HTTP read. The new API uses the details query and fails unavailable against an older owner. `ListProjectInvitations` is a read facade over the existing isolated Waldur invitation integration. Project owns its current administrator decision and canonical Project scope; Party Governance retains invitation claim ownership. The provider adaptor checks the actual Project/customer and every invitation's scope after complete pagination. It returns at most 256 pending, unexpired invitations, or fails the whole read. It introduces no invitation aggregate or mutation. The returned Project revision versions permission evidence, while `read_at` records successful read completion. Provider pagination has no atomic collection revision.