bmd-compute serves a small authenticated JSON API for machine clients such as bmd-run:
| Route | Scope | Purpose |
|---|---|---|
GET /api/v1/identity |
any valid token | Service identity, API and schema versions, the caller's principal and scopes |
POST /api/v1/plans |
plan |
Resolve a structure and workflow request into the authoritative calculation plan |
PUT /api/v1/attempts/{attempt_id} |
prepare (prepare only) or submit (prepare and submit) |
Prepare, or prepare and submit, one calculation; idempotent per attempt ID |
GET /api/v1/attempts/{attempt_id} |
read |
The attempt's authoritative state, job identity and scheduler state |
identity and plans never contact POWER. The attempt routes use Compute's
existing remote preparation, submission and monitoring machinery with
server-owned connection profiles; see Execution. There is no
machine route for cancellation or result retrieval. The browser routes are
unchanged and do not use this authentication. The API is not listed in
/openapi.json.
POST /api/v1/plans calls main.plan_calculation_request, which runs the same
module-level functions, in the same order, as the browser's Build route:
workflow_spec_from_form, resource_request_from_form, parse_structure,
resolve_workflow_for_structure (automatic treatments),
method_considerations_for_workflow_state, and
build_submission_state_from_structure (new-calculation admission, generated
inputs, submission specification). The API adds no methodology. Its only
additions are request-shape validation and classification of two input errors
the models raise as plain exceptions (unknown stage/theory/modifier names, and
CIF parser failures), so machine callers receive a classified 422 instead of a
generic failure.
The response projection reads the server-built submission specification and
regenerates per-stage inputs with generated_input_stage_previews, the same
generator the browser's multi-stage preview uses.
The intended transport is an SSH tunnel from POWER that terminates on the
service host, for example forwarding a POWER loopback port to
127.0.0.1:8000 on the VM. By default the API refuses any client whose address
is not loopback (127.0.0.1 or ::1), before reading credentials. Do not send
bearer tokens over plain HTTP across the network. Setting
BMD_API_ALLOW_NON_LOOPBACK=1 disables the loopback check; use it only behind
HTTPS.
Query-string credentials are not accepted, and any request with a query string
is refused. Never put a token in a URL: a web server or proxy may log the full
URL before the application sees and rejects the request, so refusing it does
not keep a token out of access logs. Send tokens only in the Authorization
header.
Every request needs Authorization: Bearer <token>. Tokens have the form
bmdc1.<token_id>.<secret> (32 random bytes). The service reads the token
store named by BMD_API_TOKENS_FILE on every request:
{
"schema": "bmd_compute.api_tokens",
"schema_version": 1,
"principals": [
{
"principal": "lee-benchmark",
"token_id": "<16 hex characters>",
"verifier": "sha256:<64 hex characters>",
"scopes": ["plan"],
"enabled": true
}
]
}- The file must be an absolute path outside the repository, a regular file of
at most 64 KiB, owned by the service account and not readable or writable by
group or others (POSIX). Otherwise the API answers 503
api_not_configured. - Only the SHA-256 verifier is stored. Verifiers are compared in constant time.
- Scope vocabulary:
read,plan,prepare,submit. An unknown scope makes the whole file invalid.plannever grants execution:prepare,submitandreadare separate scopes (see Execution). - Setting
enabledtofalse, or removing an entry, revokes a token on the next request. - Generate a token with
python -m compute_api.tokens --principal NAME --scope plan. It prints the token once and the entry to add to the file; it writes nothing.
Order of checks: query string, loopback, token store, token, scope, then the
request body. A missing, malformed, unknown, tampered or disabled token gets
401 unauthenticated; a valid token without the route's scope gets 403
insufficient_scope. Tokens and Authorization headers are never logged or
returned.
{
"structure": {"format": "poscar", "text": "<POSCAR or CIF text>"},
"workflow": {"desired_output": "energy_only"},
"resources": {"cpus": 48, "memory_gb": 128, "walltime": "24:00:00"}
}structure.format:poscarorcif(case-insensitive).structure.text: at most 2 MiB. The whole body: at most 3 MiB,application/json.workflow: exactly one ofdesired_output(a BMD Compute Desired Output identifier) orcustom({"stages": [{"stage_type", "theory", "modifiers", "options"}]}, at most 16 stages). Custom workflows go throughvalidate_user_workflow_spec, which rejects free-form INCAR/KPOINTS and BMD-generated options.resources(optional; each key optional):cpus,memory_gb,walltime(H:MM:SS, up to four hour digits),queue. An omitted key means Automatic: BMD Compute resolves it with the same estimator as the browser (docs/automatic_resources.md). Explicit values are kept exactly and validated by the existingExecutionResourcesallowlists. Nodes, account, partition and QOS are not accepted.- Every object is closed: unknown fields (for example
cluster,paths, SSH settings, scripts, commands) are rejected with 422invalid_request, naming the field but never echoing its value.
schema: "bmd_compute.api.plan", schema_version: 1, api_version: "v1":
plan_digest,plan_digest_version;request: structure format, workflow mode and Desired Output;structure: formula, reduced formula, atom count, space group, crystal system, volume and the canonical-structure SHA-256;workflow: recipe and ordered stages (stage type, theory, modifiers, stage options including frozen DFT+U parameters, VASP executable);automatic_treatments: the record stored insubmission.json(applied, omitted, not applicable and advisory treatments, prepared DFT+U record), ornullfor Custom workflows;method_considerations: policy version and, per consideration, its id, method, modifier, status, selection state, automatic-application state (applied,omitted_unsupported_combination,not_applicable,advisory), trigger elements and classes, applicable stage types, policy source, and the stages it was applied to;resources: nodes, CPUs, memory, walltime, partition, account and QOS;resource_selection: automatic-resource policy, the source (automaticormanual) of each resource, and the walltime recommendation with its basis (empirical_estimateorconservative_fallback), explanation and per-stage classes and allowances;scientific_inputs: POTCAR functional and, per stage, INCAR settings and their text hash, a KPOINTS summary and text hash, POTCAR symbols and a POSCAR summary and hash;software: loaded modules and the parity-critical package versions;policies: admission, automatic DFT+U, method considerations, Custodian, runtime parity, automatic resources and plan-digest versions;capability_schema_version.
The response never contains submission identity tokens, attempt IDs or fingerprints, SSH profiles, remote or environment paths, runner settings, SLURM scripts, provenance blocks or raw exception text.
Errors: {"api_version": "v1", "error": {"code", "message", ...}} with codes
query_not_allowed (400), invalid_json (400), unauthenticated (401),
loopback_required (403), insufficient_scope (403), request_too_large
(413), unsupported_media_type (415), invalid_request (422, with fields),
structure_invalid (422), calculation_invalid (422, with suggestion and
diagnostic_code when Compute provides one, for example
hse06_soc_not_supported_for_new_calculations), plan_failed (500, generic)
and api_not_configured (503).
plan_digest (bmd_compute.plan_digest v1) is the SHA-256 of canonical JSON
built from the server-built plan. It identifies what would be calculated and
with which resources, and complements the submission-attempt fingerprint, which
it does not replace.
Covered: the parsed structure (lattice, species, fractional coordinates, site properties); the resolved workflow (stages, theories, modifiers, options); the automatic-treatment record; per stage the VASP executable, INCAR settings, KPOINTS text, the SHA-256 of the generated POSCAR text and POTCAR symbols; the POTCAR functional and symbol record; nodes, MPI tasks, memory, walltime, partition, account and QOS; the resource selection (sources, recommended walltime, basis and stage classes); loaded modules and the VASP command template; parity-critical package versions; and the versions of the admission, automatic DFT+U, method-consideration, Custodian, runtime-parity, automatic-resource and plan-digest policies (plan-digest version 2).
Not covered: run timestamp, run name, label and creation time; attempt ID,
fingerprint and identity token; remote paths, POTCAR repository location,
runner and log paths; SSH profile; the SLURM script text; the Git commit and
runtime-package manifest. A POSCAR comment line or whitespace that parses to
the same structure gives the same digest. The authoritative definition is the
docstring of compute_api/plan_digest.py.
Execution additionally needs BMD_API_STATE_DIR: an absolute path to an
existing directory outside the repository, owned by the service account and
not accessible to group or others (mode 0700). It holds the attempt ledger
(one JSON file per attempt under attempts/, plus ledger.lock). Without it,
or if it is unsafe, the attempt routes answer 503 execution_not_configured.
Planning does not need it.
The plan scope never grants execution. A token needs prepare for
submit: false and submit for submit: true, and read to look an attempt
up. A token holding neither prepare nor submit is refused before the body
is read.
attempt_id is chosen by the client and must be a canonical lowercase RFC 4122
UUID (for example from uuid.uuid4()). The body is a plan request (same fields
and rules as POST /plans) plus:
expected_plan_digest(required): theplan_digestthe client obtained fromPOST /plansand intends to run;submit(required boolean):falseprepares only,trueprepares if needed and submits;labels(optional):campaignandcell, short identifiers ([A-Za-z0-9][A-Za-z0-9._:-]{0,63}), recorded for provenance.
SSH settings, remote paths, job IDs, run timestamps, account, partition and node counts are rejected like every other unknown field.
schema: "bmd_compute.api.attempt", schema_version: 1: attempt_id,
plan_digest, state, labels, created_at (UTC), effective resources,
submission (requested, job_id, submitted_at_local as recorded by
Compute) and scheduler (null until a job exists; then available,
summary of PENDING, RUNNING, SUCCESS, FAILURE or UNKNOWN, the
normalized SLURM state such as COMPLETED or TIMEOUT, exit_code,
elapsed, started_at, ended_at, terminal and checked_at).
state is one of:
| State | Meaning |
|---|---|
registered |
Bound to this principal and plan; not yet prepared on POWER |
prepared |
PREPARED on POWER; not submitted |
submitted |
sbatch returned a job ID, recorded on POWER |
submission_uncertain |
The remote attempt is SUBMITTING: a submission is in progress or its sbatch outcome is unknown. It is never resubmitted automatically. |
GET /api/v1/attempts/{attempt_id} returns the same projection after reading
the remote attempt state and, when a job exists, monitoring it through the
authenticated job record (job_<id>.json must name this attempt). A job ID is
never accepted from the client.
The first PUT for an attempt ID resolves the plan, checks that its digest
equals expected_plan_digest, and then atomically creates a ledger record
binding the attempt ID to:
- the authenticated principal;
- the SHA-256 of the validated request (structure, workflow, resources, labels);
- the plan digest;
- the run timestamp used to build the submission specification (unique among ledger attempts, so attempts never share a run directory);
- the resulting submission-attempt fingerprint.
Every later PUT for that ID must come from the same principal (otherwise 403
attempt_forbidden) with the identical request (otherwise 409
attempt_request_mismatch).
An exact retry of an attempt whose submission has already started (ledger
submission status reserved, uncertain or submitted, and expected_plan_digest
equal to the recorded plan digest) is recognised from the ledger alone, before
the current planning policy is applied, and is reconciled from the remote
attempt state: SUBMITTED returns the recorded job and SUBMITTING answers
submission_uncertain, without replanning or resubmitting. This keeps retries
idempotent for attempts registered under an earlier plan-digest version or
resource policy.
Any other PUT for an existing attempt, including a reserved attempt whose
remote state is still PREPARED, rebuilds the submission specification with the
recorded run timestamp and attempt ID, so the run directory, run name and
fingerprint are unchanged, and it must still resolve to the recorded plan
digest (409 plan_changed) and fingerprint (409
attempt_fingerprint_mismatch). An attempt registered under an earlier policy
that still needs preparation or submission therefore fails closed and needs a
new attempt ID.
The remote attempt state on POWER stays the authority. The PUT then:
- reads it through a server-owned connection; a state whose attempt or
fingerprint differs is never touched (409
attempt_fingerprint_mismatch); - if there is no remote state, prepares with
prepare_remote_submission; - if it is PREPARED: refuses (409
runtime_package_changed) when the prepared runtime package differs from the running service's, because a prepared attempt is never re-prepared or replaced; returnspreparedwhensubmitis false; otherwise atomically records submission intent and callssubmit_remote_workflow; - if it is SUBMITTED: returns the recorded job, without submitting again;
- if it is SUBMITTING: answers 409
submission_uncertain.
Compute's existing remote mkdir lock and PREPARED -> SUBMITTING -> SUBMITTED
transitions serialize concurrent duplicate PUTs, so one attempt produces at
most one sbatch. After a lost response or timeout the client repeats the
same PUT or calls GET; it never needs a new attempt ID to find out what
happened.
| Outcome of a submission call | Result |
|---|---|
| Job submitted (now or earlier) | 200 submitted with the job ID |
sbatch ran and refused the job |
502 submit_failed; remains prepared; the reservation is released |
sbatch did not run (busy, lost connection, another request holds the lock) |
503 remote_busy or 409 submission_not_started; the reservation is kept until a retry resolves it |
sbatch outcome unknown |
409 submission_uncertain; never resubmitted |
| Outcome could not be read back | 503 submission_outcome_unconfirmed; GET or repeat the PUT |
Machine plans and attempts use the same automatic resource selection and the
same ExecutionResources validation as the browser. CPU and memory values must be among the browser's offered
choices, walltime uses the same accepted value, and partition and account are
always Compute's server-owned values. The API does not add separate CPU,
memory, walltime, active-job, submission-rate or attempt-rate ceilings; POWER
and SLURM remain responsible for allocation limits, scheduling and fair share.
The ledger still records submission intent under its exclusive lock. This is part of retry and exactly-once submission semantics, not a job quota.
In addition to the planning errors: 403 attempt_forbidden; 404
attempt_not_found; 409 plan_digest_mismatch (with the resolved
plan_digest), attempt_request_mismatch, plan_changed,
attempt_fingerprint_mismatch, runtime_package_changed,
attempt_in_progress, submission_not_started, submission_uncertain (with
the attempt projection) and remote_state_invalid; 422 invalid_attempt_id;
502 prepare_failed and submit_failed; 503 execution_not_configured, remote_busy,
remote_unavailable, remote_state_unconfirmed and
submission_outcome_unconfirmed. No error contains remote output, paths,
tracebacks or credentials.