# DD — Launch acceptance (3B)
[Common design](dd.md) · [Tasks and completion status](adr.md).
Execution Core checks a request to run a saved Workload and records its decision.
`planning` means accepted and waiting for planning. `rejected` includes the reason.
## Flow
1. Receive the Workload ID, Project, initiating member, expected Workload version
and revision, required `environment: k8s | slurm`, and request/retry identifiers through the internal command.
2. Check the member's current `runs.launch` permission through the existing
`ExecutionAuthority` port. Provisioning will supply this from Project membership.
3. Ask Workload Portfolio through `WorkloadLookup` whether that Workload exists,
belongs to the Project, is active and matches the expected version and revision.
4. Save the request and decision in Execution Core's PostgreSQL database, together
with its receipt, in one transaction.
5. Later planning reads the accepted input, including its member and Project.
It uses the resource query to check the selected environment and choose its ready resource.
```mermaid
flowchart TD
R["Internal launch request"] --> E["Execution Core"]
E --> P["Check current Project permission"]
P --> W["Check saved Workload"]
W --> D["PostgreSQL
Request and decision"]
D --> A["Accepted
Waiting for planning"]
D --> X["Rejected
Saved reason"]
```
Workload and permission lookups currently use **test-only doubles**. Their real
implementations must be supplied explicitly; production has no fallback mock.
The input concerns a saved Workload, regardless of how it was authored.
## Baseline and adaptations
Baseline: architecture-reset `48e60e542`,
`request-run-launch.command-handler.ts` and `run-launch-store.ts`.
Keep its Project/Workload checks, current permission check, `planning` state and
Workload-scoped retry key. Keep the initiating member in the planning input, as
required by 1B.
Adapt persistence to the skeleton's private database, one-aggregate transaction
writer and receipts. A launch decision is a
`run_launch` aggregate; planning creates the Run and initial Attempt later.
Expected business failures also save the request and rejection reason.
This slice accepts saved immutable Workload revisions. The baseline's mutable
Notebook `workingInputs` require later authoring integration and receive an explicit
`working_inputs_unsupported` rejection. Planning commands, reserved Run identities,
leases and worker invocation belong to the following processing work. Internal planning
is described in the [planning DD](run-planning.md).
## What is saved
| Value | Meaning |
| --- | --- |
| `id` | The first request's canonical UUID; identifies this launch decision |
| `request` | Original envelope and payload: member, Project, Workload/revision, expected version, environment and correlation/retry metadata |
| `decidedAt` | Explicit decision time supplied to the handler |
| `status` | `planning` or `rejected` |
| `rejection` | Code and explanation when rejected |
The stored aggregate begins at version 1. The request's `aggregateVersion` is the
expected **Workload** version; it is not the launch aggregate's version.
Malformed snapshots fail restoration. Owner calls happen outside the transaction.
Launch acceptance uses an explicit `None` entry in the shared writer's projection
registry. It saves the request, decision and receipt without realtime rows or a
launch browser-update contract. A missing registry entry remains an error.
The launch-update schema, generated classes, encoder, Centrifugo publication,
subscriptions and browser views belong to **Milestone 2, subtask 7A**. That work
will enable atomic notification recording alongside state changes and use an
authorised initial read for launches saved before notifications were enabled.
## Retries and current access
- Resending the same request returns its original decision.
- A new delivery UUID with the same Workload and retry key also returns that
decision, provided its material inputs match. It keeps the original launch and
adds a receipt for the new delivery. The original request UUID remains the result ID.
- Changing the Project, member, version, revision, environment, contract version
or deadline under that key is an `idempotency_conflict`. Reusing a delivery UUID
with a different envelope also conflicts.
- An accepted replay still requires current Project permission and a Workload in
that Project. A subsequent Workload revision does not change the saved decision.
- A rejected request stays rejected. A revised submission uses a new retry key.
- Permission/Workload lookup outages raise retryable errors rather than recording
terminal rejections. Revocation blocks returning an accepted replay. The original
acceptance remains historical state; the 1C gate must check again before execution.
Migration `0001_launch_retry.sql` adds two unique indexes to the existing aggregate
and receipt tables. These enforce the retry-key and delivery-ID scopes without
restoring the baseline's shared `platform.*` schema. Runtime keeps its restricted role.
## Implementation and evidence
- [Acceptance handler and owner ports](../../../contexts/execution_core/application/launch_acceptance.py)
- [Saved launch and restoration](../../../contexts/execution_core/domain/launch.py)
- [PostgreSQL retries and transaction composition](../../../contexts/execution_core/adaptors/launch_requests.py)
- [Behaviour tests](../../../tests/execution_core/test_launch_acceptance.py)
- [Real PostgreSQL tests](../../../tests/infrastructure/launch_acceptance.py)
| Verification | Evidence |
| --- | --- |
| Acceptance rules | 30 new deterministic cases: permission, Workload scope/state/version/revision, malformed input, replay and corrupt state |
| PostgreSQL persistence | 28 launch cases passed, within 54 combined Execution Core cases: reload, concurrent retries, conflicts, corrupt state, atomic rollback and zero launch realtime rows |
| Repository checks | `pnpm verify` passed, including 172 Metallo unit cases, generated-contract drift, types, lint and architecture checks |
| Disposable service/browser integration | `pnpm test:integration` passed: 55 PostgreSQL cases, RabbitMQ, service images, sessions and the existing browser integration test |
| Isolated dev-box harness | 208 cases passed: 154 Execution Core unit cases and 54 real PostgreSQL cases, with mocked external owners |
| Workload Portfolio and Project permissions | Mocked; real owner connections remain unchecked in ADR 3B |
| Native execution | Pending the later runtime tasks |
Verified on 9 October 2026 after removing launch notifications. The isolated
`dev-dkats` harness used image `pharos-metallo/identity:851a06c02b21` and namespace
`metallo-identity-dev-dkats-851a06c02b21`. Its private test log and saved-state
inspection are retained under
`.local/metallo/identity-tests/dev-dkats/identity/metallo-identity-dev-dkats-851a06c02b21/report.json`.
The temporary namespace and database were removed after success.
PostgreSQL, migrations, runtime restrictions and existing execution-message codecs
are real. Workload, provisioning, permission and credential providers are mocked.
Native execution and Minikube/UAT launch flows remain later integration work.
Full 3B remains open until the real Workload and membership queries are connected
and verified. The completed internal and mocked steps are tracked individually
in the [ADR](adr.md).
The environment change and its regression evidence are recorded in the [planning DD](run-planning.md); the table above records the earlier 3B verification.