> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Resource Provisioning (working name)

Proposed owner of the execution-facing facts about a Project's resources at a Site (#763): allocations, their
lifecycle, configured and applied limits (#764), and later the provisioned native accounts and applied access (#765).
The name, service and boundary await the ownership decision recorded in the
[context catalogue](/reference/docs/context-catalogue.md); the design is in
<a href="/source/services/metallo/docs/resource-site/design.md.txt">the Resource and Site design</a>.

It does not own ordering, approval or billing (Waldur), identity or Projects (Beto), or Site-local enforcement (Site
Runtime). It never reads Waldur or a Site with privileged credentials as a fallback for a refused decision.

## Allocation

One `Allocation` per provider resource (`domain/model.py`). Its canonical ID is derived from the resource; the
Waldur resource, offering and Project and the Site's own name (a namespace) are opaque bindings.

- **Source.** The Site agent that fulfils the resource reports it (`domain/observation.py`): provider state,
  paused flag, configured limits and the limits it applied, in canonical units (CPU millicores, memory and storage
  bytes, accelerators as whole devices). Translation from provider units happens at the boundary, without rounding.
- **Project.** The provider Project is resolved to Beto's canonical Project through the `ProjectBindings` port before
  the transaction. Until it resolves, the allocation is `pending` with `project_unbound`; a binding once proven is
  kept.
- **Lifecycle.** `pending` (with reasons: `project_unbound`, `provisioning`, `provider_erred`,
  `limits_not_applied`), `ready`, `changing` (a limit change not yet applied; the previous applied limits stay in
  force), `suspended` (the provider paused it) and `retired`, a final tombstone.
- **Revisions.** `revision` counts material changes, each a command with its receipt and a new version. A newer
  report that changes nothing material only renews the time the allocation was last seen, so freshness never looks
  like a change. Older reports are `stale_report`; the same report again changes nothing.
- **Usage.** A report can carry what the Site's quota counts as used (`used`, #1074). Usage is not material: new usage
  is saved with a new version but no new revision, and the same usage again only renews the time it was seen.

State, version and command receipt commit together in this context's own database through the generic aggregate
store (`adaptors/postgres.py`); a snapshot that differs from its row is corruption. An unchanged report is not a
command: it updates `allocation_sightings` (migration `0001`) only while the allocation is still at the version it
was compared with, and otherwise is decided as a command. Agents report every resource every minute, so this keeps
receipts and versions growing only with real changes. `SavedAllocations` gives validated reads by ID, by Project and
as a complete paginated snapshot for initial loading and repair; each read, and each command's stale check, uses the
later of the snapshot's `observedAt` and the last sighting.

## Project view

`ProjectView` (`domain/project_binding.py`, `application/projects.py`) is this context's local view of Beto's Project
bindings, built from Beto's `project.ProjectChanged` integration events (`adaptors/project_events.py`): Project,
revision, lifecycle and provider binding. Each event is applied once under a consumer receipt, older revisions change
nothing, and a provider Project resolves only when exactly one viewed Project holds it. The binding is identity, not
access, so a closed Project still resolves. Tests deliver real encoded Beto events to `consume_project_changed`;
the runtime composition uses the shared Party and Project view described below.

This view is interim. Its replacement is this context's copy of the shared Party and Project view (#780), the tables
every enrolled Metallo context keeps (migration `0002`), with Beto's latest Project authority beside them (`project_authority`,
migration `0005`), which the shared view keeps since #817. `adaptors/view_projects.py` answers `ProjectBindings` from it
by the Waldur Project, as Beto keeps it, and is unavailable until the view's initial load has matched Beto's
checkpoints. `adaptors/view_site_bindings.py` answers `SiteBindings` from the same copy's Party provider bindings: the
established `site` binding holds a username over a retired one, and conflicts and an unloaded view are not an answer.
A member's generation, which advances when a Party returns, is not in those tables, since a snapshot replaces the
members whole. `adaptors/member_generations.py` keeps it in `member_generations` (migration `0003`): a view subclass
records it in the same transaction that replaces the members, from an event or a checkpoint repair.
`adaptors/view_roster.py` answers `MemberRoster` from the `site` binding, Beto's `project_allowed` decision for
membership, and that generation.
The service's `ProvisioningRoster` requires ready, unexpired Project authority matching the viewed revision before
returning an active member. Pending or missing authority defers the read; absent and retired members retain their result.

Beto enrols this context for its views as the `pharos_resource_provisioning_views` caller in its broker contracts, whose
credential is `RESOURCE_PROVISIONING_VIEWS_BROKER_URL`. Metallo's process keeps this context's copy of the view with the
eight consumers and the synchroniser (`services/metallo/adaptors/provisioning_beto.py`), and gives the Site intake the
Project lookup and member roster. Migration `0005` adds Beto's Project authority to the view. The retained list is
answered only after accepted-work recovery completes (below); until then the agent keeps every account. A Site intake without the credential or
Beto's source scope fails startup. The interim view above stays until the routes no longer use it.

### Accounts kept for accepted work (#765, #766)

A member who leaves a Project loses personal access at once, but their exact account stays for the work the Project
already accepted. `adaptors/accepted_work.py` keeps the items Execution Core publishes, one per accepted Launch, Run or
Pipeline, in `accepted_work` (migration `0004`): Party, Project, Site, the `site` binding and member generation of the
account, the Project's management generation at acceptance, an absolute deadline and a state, `required` or
`released`. A revision orders an item, and a release is final. Reaching the deadline is not a release.

`GET /resource-provisioning/<site>/retained-accounts` answers a Site agent with one complete list of the accounts to
keep. An account is listed while an item for it is required and nothing has fenced it: the Project is no longer active
or has another management generation, the Party's `site` binding is no longer established, or the Party was added again
and so has a newer generation. All of that is decided from this context's own copy of the shared view. An item that
names a Project or binding the view does not hold, or a view that has not loaded, makes the answer a 503, never an
empty list, so an agent keeps every account.

Execution Core and Pipeline Coordination publish the items as `AcceptedWorkChanged`, and three consumers in the
`metallo` process save them (`adaptors/provisioning_accepted_work.py`): one per owner's event, declared as
`resource_provisioning.accepted-launch-changed`, `-run-changed` and `-pipeline-changed`. Work saved before acceptance
recorded its evidence has management generation 0 and no binding, so it keeps no account and is not saved; a binding
with generation 0 is refused. The events add to what this context holds from the moment it subscribes. Startup
recovery checks that the three durable queues exist, then reads both owners through
`RESOURCE_PROVISIONING_QUERIES_BROKER_URL`, the scoped `pharos_resource_provisioning_queries` identity. It verifies
every page against the complete inventory fingerprints and counts before applying the recovered items. Changes
during pagination or owner outages retry with back-off; successful recovery is compared again daily. Each item
follows the same revision and final-release rules as events, so delayed required snapshots cannot undo a newer
release. An item absent from recovery is kept until an explicit release arrives; both owners list released items
for that reason, so a pass delivers a release whose event was missed. See the
<a href="/source/services/metallo/docs/epics/instantiate-and-run/accepted-work.md.txt">owner recovery contract</a>.

The `retained-accounts` route answers from this store (`RecoveredRetention` in `adaptors/provisioning_beto.py`), but
only while a complete recovery pass stands. The events add only what the owners publish after this process subscribed,
so before the first pass after a start the store may lack work that still needs an account. While a pass has not
completed, including while the daily repair runs again or fails, the route answers 503 and the agent keeps every
account. Upgrade by applying migration `0005`, rerunning broker administration and delivering the new query
credential before starting the consumer.

## Members and native accounts (#765)

The same event lists the Project's members. The view keeps those whose Party has a verified provider binding (the
binding ID and revision Beto names); a Party still pending in Beto is not kept, and so has no account. Each member has
a `generation`: it advances when the Party is added again after leaving or when its verified provider binding is
replaced, and a member who left stays as an inactive tombstone with the generation it had. A later account of the same
name therefore cannot revive an earlier grant.

`adaptors/provisioning_accounts.py` (service level) answers Execution Core's `ProvisioningAccounts`: one account,
the account the Site confirmed for an active member of a Project that has an allocation which is not retired, and none
otherwise. Site and allocation selectors are applied before checking native confirmation, so an unfinished
Kubernetes account cannot block a selected Slurm account. Unreadable or corrupt evidence within the selected
scope is `LookupUnavailable`, never "no account". A complete recent report
without the member's binding establishes absence for that allocation. Other confirmed allocations remain
selectable; when every matching account is absent, the caller receives `AccountAwaitingSite` with reason
`not_confirmed`. A report for the same binding at another
generation leaves admission pending until the current generation is confirmed; older generations alongside that
confirmation do not prevent admission. Multiple confirmed accounts at one Site, including distinct Site bindings,
remain ambiguous. Retained execution continues to wait for its exact saved account when confirmation is unavailable.
An account awaiting Site confirmation raises `AccountAwaitingSite`, a `LookupUnavailable` carrying
the confirmation reason, so a new launch can explain the refusal to the member (#1119).
The account's name is the one the
Site reported, not one derived here. The Site agent reports the accounts it finds to
`POST /resource-provisioning/<site>/accounts` (`application/accounts.py`, `adaptors/native_accounts.py`, migrations
`0006` to `0008`): the complete set for a resource, each account as `ready` or `incomplete` by its Party, the name the
Site gave it, the Site's immutable UID of the account, `site` binding and member generation. The name is the Site's
fact: a Kubernetes Site names the account `member-<Party>` and a report that names it otherwise is refused, while a
Slurm Site names it with the POSIX login name it made for the member (`[a-z_][a-z0-9._-]{0,31}`, refused otherwise),
because the Party does not determine a directory name. The stored name is what `ConfirmedAccounts` hands back as
`account_reference`, so a Run acts as the account that exists rather than as one reconstructed from the Party. The UID
is part of the key, so an account made again under the same name is a new row, the old one stays `absent`, and
`ready()` returns the UID for a reader to compare. The set replaces the previous one, accounts of older generations
stay as evidence, a report older than the saved one is refused, and nothing a report says grants or removes an account.

`application/confirmed_accounts.py` reads it back for a context that acts only once the account is there
(`adaptors/native_account_reads.py`, one read that returns the report time and the Party's accounts together):
`ConfirmedAccounts.get(allocation, project, site, party, binding, generation)` names the account as accepted work saves
it, so it still answers after the member has left. It answers with the name the Site reported and the UID the Site gave
the account, not a name derived from the Party, plus when the Site reported, when the Site's latest report lists exactly
that binding and generation as `ready` and the report is recent: under the configured age limit (fifteen minutes by
default) and at most five seconds
ahead of this clock, since the report time is the agent's own. The allocation must be the Project's own at that Site and
not retired. Anything else is `AccountUnconfirmed` with a reason. `not_reported` and `stale_report` mean the Site has
said nothing recent, so nothing is known and the caller waits; `incomplete` means the account is there without all its
objects; `not_confirmed` means the Site's recent report, which lists every account it found, does not list that binding
and generation, so the account is not there now. `generation_pending` distinguishes a report listing that binding
only at other generations, which requires retry. Unreadable evidence, including a stored observation time without
a timezone, is `CorruptState`. It answers; it grants nothing, and whether the Party should
still have the account is the Project view's answer.

### Report freshness and activation

The request host, execution worker and Site API read `RESOURCE_PROVISIONING_ACCOUNT_MAX_AGE_SECONDS` at startup.
It defaults to 900 seconds and accepts whole seconds from 1 to 86400; invalid values stop startup before opening
connections. Set the same value on all three processes, allowing several membership-sync cycles and expected
reporting delay. The default covers three cycles of the agent's upstream five-minute
`WALDUR_SITE_AGENT_MEMBERSHIP_SYNC_PERIOD_MINUTES`; the shipped membership deployment sets one minute.
At or beyond the age limit, all affected admissions and native confirmations stay pending with a retryable 503.
The Site API uses the same limit to cap execution and private-read authority at the account's observation time
plus that limit. Project authority, the accepted-work deadline or private-read permit, and the 60-second response
lifetime can each shorten the returned validity.
Increasing the sync period requires reviewing the limit. Process health alone does not establish report freshness.

These processes use `RESOURCE_PROVISIONING_DATABASE_URL`, the owner's read-write `pharos_resource_provisioning`
credential. They can rewrite native account evidence, aggregate state and projections, so they share that database
trust boundary. [#1070](https://github.com/pharos-ai-factory/pharos-platform/issues/1070) tracks removing this write
authority from all three readers through restricted read-only access or an authenticated owner query service.
This is deferred hardening for #896; the shared write authority remains until that follow-up is complete. The current readers
need migration `0008` for Site-reported account names; the earlier `0006` report tables and `0007` native-UID key are
included in that upgrade.

Keep execution deployments paused until the Site reporting prerequisites and #771 qualification are complete:

- **Kubernetes:** the current shared agent template takes `identity_source` from the environment;
  <a href="/source/devops/platform/environments/common.yml.txt">`common.yml`</a> defaults it to `metallo`.
  The dev-box configuration supplies `identity_metallo_url` and `identity_metallo_key_file`. Deliver the matching
  Site intake key to Metallo and the agent, connect the Resource Provisioning Beto view, and verify one complete fresh
  report for every allocation before enabling requests. Any environment still selecting `waldur-stand-in` sends no
  account confirmations. Existing stand-in accounts need canonical Party, binding and generation metadata.
- **Slurm:** the `WALDUR_HPC_*` reconciler merged in #973 writes account reports directly through `NativeAccounts`;
  its settings and current evidence limitations are described [below](#the-hpc-offerings-names-765).
  Keep HPC execution gated by #771 until [#1068](https://github.com/pharos-ai-factory/pharos-platform/issues/1068)
  replaces the derived confirmations with evidence of the identity applied at the Site, its observation time and
  the complete inventory of accounts retained for accepted work. This addresses the missing trustworthy Slurm
  confirmation raised in #896's review. The current Waldur polling report does not satisfy those activation conditions.

For the first Icarus activation, the proposed order for #1068 is:

1. Establish the Site's applied identity: persist the exact allocation, Party, binding and generation, and verify
   both the directory account and that allocation's Slurm association before confirming it. Preserve the existing
   username assignment.
2. Deliver complete native inventory, including accounts retained for accepted work, with the Site observation time
   and durable report identity/content. Incomplete discovery stays pending; retries and unchanged Waldur polls cannot
   renew evidence or confirm a later membership generation.
3. Coordinate account incarnation with #974 so recreating an account with a reused POSIX UID cannot revive accepted
   work, then qualify real Slurm Runs under #771, including leave/rejoin, retained work after departure, missing Slurm
   associations, reporting outages, incomplete discovery and restart/retry recovery.

This sequence is a proposal for review. Each condition applies before activation; execution stays paused until
the agreed implementation and real journey are qualified. #1070 remains the separate, non-blocking reader-privilege
follow-up described above.

A Site agent asks for the verified member behind a username with
`GET /resource-provisioning/<site>/members/<username>?providerProjectRef=<ref>` (`application/roster.py`). The answer
joins the Party's `site` provider binding, which names the Party for a username, with the Project view's members, whose
binding is the Project's own provider's and so cannot answer alone. It returns the canonical Project and Party, the
`site` binding's ID and revision, the member's generation and whether that binding is `active` or `retired`. Only a
Project with an allocation at the asking Site is answered, so a Site's key cannot probe other Projects; anything
unreadable is a 503 and never "not found". `services/service.py` composes the route with `ProvisioningRoster` over this
context's Beto view. Configuring Site intake without its view broker credential and enrolled source scope stops
startup. An uninitialised or stale view leaves the member lookup unavailable.

## The HPC offering's names (#765)

The <a href="/source/services/metallo/contexts/resource_provisioning/offering-names.md.txt">Site offering naming service</a> also supplies the
Kubernetes Cloud offering from the Party's `k8s` binding. Kubernetes account
confirmation continues to arrive from its Site Agent.

An HPC Site's agent reads an account's name from Waldur and never invents one, so the name has to be on Waldur's
offering user before the agent provisions. Metallo writes it there and reads the result back, with one credential: an
`OFFERING.MANAGER` token of the HPC offering (`OFFERING.UPDATE_USER` is what naming an offering user requires). The
pass runs every minute in this process and only when all four settings are present:

| Setting                     | Meaning                                                                                                                              |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `WALDUR_API_URL`            | The Waldur API root, ending in `/api/`.                                                                                              |
| `WALDUR_HPC_OFFERING_TOKEN` | The offering's manager token, read like every other secret (`_FILE` works).                                                          |
| `WALDUR_HPC_OFFERING_UUID`  | The offering whose users are named.                                                                                                  |
| `WALDUR_HPC_SITE`           | The Site's environment, the key its `site` binding is stored under (`slurm`); a Site ID such as `icarus-hpc` is refused at start-up. |

The four settings reach the process from the installation that serves an HPC offering: the deployment loads the
`metallo-hpc-offering` Secret and the `hpc-offering-settings` ConfigMap as optional, single-key environment references
(`gitops/manifests/pharos/base/metallo/workload.yaml`), so a box without them gets no variables at all. With none of
the four set the pass logs `disabled` and does not run, which is the normal state of a box with no HPC offering; with
some of them set it logs which are missing and refuses to start, so a half-configured pass never names users from
another Site's bindings.

For the HPC allocation pilot, `make hpc-site-central HPC_ALLOCATION_LIMITS='<JSON>'` adds
`WALDUR_HPC_ALLOCATION_LIMITS` to the same `hpc-offering-settings` ConfigMap. It refuses a value Metallo would refuse,
and a later run without the variable keeps the delivered value. It is an optional JSON mapping in canonical units
holding the **real capacity of the Site's node**, for example:

```yaml
# Laboratory node: 16 cores, 4 GiB and two GH200 devices.
WALDUR_HPC_ALLOCATION_LIMITS: '{"cpu":16000,"memory":4294967296,"gpu.gh200":2}'
# Icarus: 144 cores and two GH200 devices. Replace NODE_MEMORY_BYTES with what the administrators report.
WALDUR_HPC_ALLOCATION_LIMITS: '{"cpu":144000,"memory":NODE_MEMORY_BYTES,"gpu.gh200":2}'
```

The values are what one compute node offers, not an invented ceiling: CPU in millicores (`144000` for Icarus's 144
cores), memory in bytes of the node's memory, and GPUs in devices (`gh200: 2` on both). A memory value below the
node's size refuses every Run above it at launch, so do not copy the laboratory's value to another Site. Slurm still decides admission and
enforces what a job may take, including Waldur's node-hour allowance; there is no conversion from node-hours to
instantaneous capacity, so these are capacities rather than a per-member quota. Positive `cpu` and `memory` are
required. Ephemeral storage is not declared: a Slurm job has none to grant and the Slurm executor refuses a Run that
asks for it. This temporary Waldur relay is separate from genuine Site reporting (#1068).

With this setting, also supply the existing `RESOURCE_PROVISIONING_WALDUR_INSTANCE` exactly as Beto names the
installation. No Slurm intake key is needed. Before naming and account confirmation, the sweep reads every resource
of the offering from `marketplace-provider-resources` with the existing token and reports it through the in-process
`ResourceProvisioning.report` handler. Waldur 8.1.3's `filter_for_service_provider` includes offerings on which the
user holds a role; its resource queryset includes Terminated resources. Pagination has no state filter, so retirement
is reported too. An OK resource with a backend account receives the declared configured/applied limits; other states
or a missing backend account remain unconfirmed. The handler still resolves the Project from Beto's local view,
leaving an unresolved Project pending. Paused resources suspend and terminated resources retire as usual.

Observation time comes from Metallo's clock. A bad resource is retried independently; a failed listing cannot stop
naming or account confirmation. Missing resources are not inferred to be terminated, and a failed read never renews
an allocation's freshness. Without `WALDUR_HPC_ALLOCATION_LIMITS`, allocation polling is disabled and the existing
naming/confirmation cycle is unchanged. The setting requires a rollout of the Metallo process to take effect.

Naming (`application/hpc_accounts.py`, `adaptors/waldur_offering_users.py`): an offering user without a username gets
the username its Party holds on the `site` binding at that Site (`adaptors/view_member_accounts.py`); a user that
already has one is never renamed. No binding, or no viewed Project listing the Waldur user, writes nothing and the
offering user keeps waiting with the reason in the log. A view that cannot be read raises and the pass retries, so an
unreadable answer is never "no username".

Confirmation: the reconciler merged in #973 is composed in
<a href="/source/services/metallo/adaptors/provisioning_hpc.py.txt">`adaptors/provisioning_hpc.py`</a>. For every live allocation of the offering, it reads
the current Project members and their Waldur offering users, then writes a complete account report directly through
`NativeAccounts` into this context's database. This path needs no Slurm HTTP intake key. A matching Party `site`
username, valid POSIX login name and OpenLDAP `entryUUID` in the resource's
`backend_metadata.accounts[username]` make an account reportable; Waldur state `OK` makes it `ready`,
and other states make it `incomplete`. Missing or mismatched identity is omitted. A report the store refuses
(stale or for an unknown allocation) is logged and retried, and nothing partial is recorded.

These reports carry the current membership generation from the Project view and the time Metallo polled Waldur.
An unchanged provider row can therefore confirm a rejoined member's new generation without a new Site observation.
Departed members are omitted from the complete report, including accounts still needed by accepted work. Repeated
polls also renew freshness while the provider row remains unchanged. [#1068](https://github.com/pharos-ai-factory/pharos-platform/issues/1068)
records the reproduced leave/rejoin and retained-account failures; the stopped-Site freshness risk is based on source
inspection and awaits live qualification. Its follow-up must establish the exact identity applied at the Site,
preserve the Site observation time and report retained native inventory before #771 enables HPC execution.
The Site must verify the directory account and the allocation's Slurm association before confirming the canonical
Party, binding and generation. Persisting that applied identity prevents an unchanged account from acquiring a
rejoined member's new generation. Preserving observation time and retry content prevents polling or replay from
renewing stale evidence. Including retained accounts keeps accepted work's saved identity available after departure.
The admission generation, freshness and saved-identity checks remain required.

The resource metadata listing uses the same offering manager token and pagination as allocation polling, with no
state filter. Confirmation reads it even when the optional allocation pilot is disabled. Agent and Metallo versions
are synchronised: an absent entryUUID omits the account, with **no fallback to uidnumber**. POSIX UID/GID stay fixed
for file ownership; the LDAP entryUUID changes on recreation. The store already keys accounts by nativeUid and marks
the previous row absent. Accepted work retains the previous entry identity and is refused after the new report;
a new Run can accept the replacement. No schema migration or extra Site connection is needed. A malformed resource
map is retried independently, and a failed listing never renews account evidence.

A resource whose `backend_metadata` carries no `accounts` key was **not observed**: an agent from before this
contract, or a rollback to one, publishes nothing there. Such a resource is left out of the listing's answer and the
pass writes no report for its allocation, so the last complete report stands. Its rows then age out under the ordinary
freshness rule: `ConfirmedAccounts` refuses a report older than fifteen minutes as `stale_report`, without any pass
marking them `absent`. An explicit `"accounts": {}` is different — the Site looked and found no account — and it writes
a complete empty report, which does mark the earlier rows absent.

Waldur's offering-user list is reachable by any authenticated user, so the token is treated as read-only apart from the
single username write. The write is refused for an offering whose `account_scope` is `provider`: an HPC Site names its
own accounts, and its users are named on the offering user itself.

## SSH Access owner queries

`application/hpc_access.py` exposes provisioning evidence for SSH Access through
`adaptors/ssh_access_resources.py` at service level. The configured offering and
Slurm environment identify the Site's HPC resources. Eligibility requires a
fresh `ready` or `changing` allocation. Account lookup checks complete allocation
pages and native reports for the exact Party and Site binding, verifies the
username and numeric POSIX UID, and defers conflicting evidence. It uses the
existing fifteen-minute account freshness bound and five-second clock skew.

Migration `0009` adds nullable `uid_number` to the native account report. The
offering-user reconciliation saves Waldur's numeric UID alongside the LDAP
`entryUUID` in the same transaction. `native_uid` keeps its account-instance
meaning for execution. SSH Access requires the explicit numeric value; existing
rows wait for a fresh reconciliation, and no value is inferred from `native_uid`.
The Site report HTTP contract remains unchanged.

The username and UID originate in the offering-user reconciliation above; SSH
Access combines this evidence with Beto's current Project capability decision.
Account lookup also permits evidence retained for former Project members, whose
saved SSH keys can still require removal. These reads publish no new event and
grant no access themselves. The SSH owner uses its saved target for revocation
when current account evidence is unavailable.

## Events

`resource_provisioning.allocation.changed.v1` (`adaptors/events.py`) is defined and registered in Metallo's codec,
but not yet published: the broker grants a publisher only for declared consumers, and an unrouted event would stay in
the outbox. Metallo's own consumers read saved allocations through a service-level owner adaptor, as they read
Workloads. Publication starts with the first declared subscriber, which loads the paginated snapshot first.

## Site agent intake

Each Site's agent posts its resources to `/resource-provisioning/<site>/reports`, its accounts to
`/resource-provisioning/<site>/accounts` and reads
`/resource-provisioning/<site>/allocations/{id}` (<a href="/source/contracts/http/resource-provisioning.v1.yaml.txt">contract</a>,
`adaptors/routes.py`). Nothing is mounted for a Site unless `RESOURCE_PROVISIONING_<SITE>_INTAKE_KEY` is set; that key
opens only its own Site's prefix, so the Site comes from the credential, never from the message. The base deployment
sets none. With a key set, `RESOURCE_PROVISIONING_WALDUR_INSTANCE` must name the Waldur installation exactly as
Beto's Project events do, so that reported Projects resolve through the Project view.

## Execution Core

`adaptors/provisioning_resources.py` (service level) answers Execution Core's `ProvisioningResources` from saved
allocations: non-retired ones only, ready while `ready` or `changing` and observed in the last fifteen minutes, with
applied limits translated for planning. A `suspended` allocation observed in that window is reported as suspended, so
planning refuses the Run with `resource_suspended` instead of waiting; an older pause is only not ready. The execution worker and request host open this context-owned connection before starting workers.

## Status

Allocations (#764) are implemented with unit and route tests, and real-PostgreSQL tests in
`tests/infrastructure/resource_provisioning.py`, as is the Project view. The runtime composes the Beto view and
member roster, and the Kubernetes agent reports native accounts when configured with the Metallo identity source.
The [activation requirements](#report-freshness-and-activation) still govern execution and Site qualification;
native account removal remains gated by #771. Allocation event publication awaits its first declared consumer.

`application/confirmed_accounts.py` and `adaptors/native_account_reads.py` expose the
last native account report for an exact allocation, Project, Site, Party, binding
and generation. Reports must be recent and ready; an absent or incomplete report
keeps execution pending. Admission records the confirmed native UID. Continuation
uses that saved identity independently of current membership and refuses a changed
UID or allocation. This is the reader introduced by #895; #830 composes it with
admission, retry and Runtime authority. Final account withdrawal still requires
\#766's cleanup-completion evidence from the Site agent.

## Project member allocation query

`MemberAllocations` reads all saved allocations for a canonical Project, including
retired records, after and before-return current Beto `project.view` checks.
The typed `resource_provisioning` request runs in the existing request host with
its context-owned database and `RESOURCE_PROVISIONING_REQUESTS_BROKER_URL`.
The API exposes `GET /api/v1/projects/{projectId}/allocations` with canonical IDs,
owner revisions, lifecycle/reasons, observation times, configured/applied
limits and the usage last reported (null until a Site reports it). Provider bindings and native account names remain private. CPU is in
millicores, memory/storage in bytes and `gpu.*` in whole devices. Applied limits
are null until confirmed; these saved observations do not constitute execution
admission or promise current readiness. Empty results and unavailable authority
remain distinct responses. The result is complete, without implicit truncation.

The retained Project page labels these values as **quota usage**, with used and
applied values in the same unit for each component. Kubernetes reports the
ResourceQuota's accounted workload requests or limits according to the Site's
component mapping; sampled CPU or memory utilisation is a separate metric. Missing usage stays unavailable, including an individual
component omitted from a report; zero remains an observed value. Ordered limits
appear while Site confirmation is pending, and changed orders remain distinct
from the limits currently applied.

The page retains the allocation ID, material revision and exact observation time.
It marks an observation older than fifteen minutes as stale and an observation
more than five seconds ahead of the browser clock as having uncertain freshness.
An expiry timer only updates this local label. Manual Refresh reads the existing
authorised owner query; there is no provider read or browser polling. A refreshed
usage value can change without changing the allocation's material revision.

<details class="page-source"><summary>Page source</summary>

<a href="/source/services/metallo/contexts/resource_provisioning/README.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
