18 KiB
Arcadia Studio server
arcadia-server is a headless Rust REST service for the same schema-4 model, commands, source language, validation, artifacts and OA/SA traces used by the desktop. It stores each model and its authored source project together in an atomic JSON envelope. The service provides a deployment artifact and configuration examples; this repository does not assert that a remote instance has been deployed.
The complete HTTP contract is api.rst; machine-readable schemas are in openapi.json, also served at /api/v1/openapi.json. The original service research discusses broader alternatives; retained history, SQLite, idempotency receipts and stored transition plans from that research are not the implemented storage contract.
Build and run
Build only the server to avoid GUI/renderer dependencies:
cargo build --release --locked -p oa-server
.\target\release\arcadia-server.exe
cargo build --release --locked -p oa-server
./target/release/arcadia-server
The default bind is 127.0.0.1:8787. Default data storage is %LOCALAPPDATA%/ArcadiaStudio/server on Windows and $XDG_DATA_HOME/arcadia-studio/server or ~/.local/share/arcadia-studio/server on Linux. Specify --data-dir for deployment, and use a local disk with adequate free space. The working OneDrive directory is not the default database location. --help and --version do not open the store.
Supported options are --bind IP:PORT, --data-dir PATH, and --token-file PATH. ARCADIA_SERVER_TOKEN is an alternative to the token file; when both exist, the file wins. Tokens must have at least 32 printable ASCII characters without whitespace. Trailing CR/LF in a token file is removed. Tokens are never printed. Generate a random secret and restrict its file permissions to the service account.
Loopback permits unauthenticated native clients unless a token is configured; unauthenticated requests must use a localhost or loopback-IP Host header. Binding any non-loopback IP requires a token. A configured token protects every model route; only health and OpenAPI are public. Requests carrying a browser Origin are rejected because the initial service has no browser/CORS integration. Artifact URIs are stored as model data and are never fetched or executed.
First API call
With the loopback server running:
curl --fail-with-body http://127.0.0.1:8787/health
curl --fail-with-body -i -X POST http://127.0.0.1:8787/api/v1/projects \
-H 'Content-Type: application/json' -d '{"name":"Harbor assistance"}'
The create response returns a service resource_id, revision and ETag. Fetch /api/v1/projects/{resource_id} to obtain the full {resource_id, revision, model, source_project} envelope. Use the exact ETag as If-Match on updates. With authentication enabled, add Authorization: Bearer <your-token>; avoid putting the token in URLs, shell history or shared example files.
The service resource ID is distinct from the editable model ID. Model IDs, aliases, diagrams, artifact URIs/revisions and trace records round-trip through source/JSON. Imported copies receive new resource IDs. An artifact's revision is an opaque external-artifact revision string; it is not a retained server model revision.
Storage and concurrency
The data directory contains .arcadia.lock and one <resource-id>.json file per hosted project. Each envelope has exactly resource_id, positive revision, model and source_project. The source bundle contains its entry filename and a map of file text; compiling it must reproduce the stored model exactly. The process keeps an exclusive OS file lock on the directory lock file for the entire store lifetime. A second process using the same directory fails at startup. Do not directly edit, synchronize or replace files while the service is running.
Startup validates every committed envelope, its filename/UUID, revision, size and complete model. Any corrupt or unsupported envelope stops startup; the service does not silently skip bad data or invent an empty replacement. Sibling .arcadia-write-* files from an interrupted pre-commit write are not committed models and are ignored on startup.
Earlier model-only envelopes are accepted through the existing model migrations and receive deterministic source files in memory. Opening, reading or an exact no-op neither rewrites those envelopes nor advances their revision. The next changed edit persists the coupled representation. Already-coupled schema3 envelopes explicitly migrate both representations in memory; the independently migrated graphs must agree. Current-format source must contain stable IDs. Inconsistent pairs are rejected at startup. Migration reports remain visible until a current-format edit/save; opening alone never rewrites the envelope. Capacity accounting includes source generated in memory for old envelopes, so an over-capacity old store fails explicitly without rewriting or pruning it.
Parsing, validation and blocking disk operations run on blocking workers. A shared store mutex captures snapshots and serializes final revision checks and writes. Parsing, semantic validation, source writeback and query processing use the captured snapshot outside that lock; an expensive candidate does not block the catalog or another model's preparation. Commit rechecks the exact ETag, so a candidate prepared against a superseded snapshot returns 412 revision_conflict. A successful changed update increments revision once. Both model and source bundle must be unchanged for changed: false; comment-only edits advance the same revision. Each write serializes the whole envelope to a sibling temporary file, syncs it, and atomically replaces the destination. Unix also syncs the directory. The in-memory snapshot changes only after the replacement succeeds. A directory-sync error after replacement is reported as an uncertain write outcome while the in-memory head still matches the installed file.
Atomic replacement protects against partially written envelopes. It does not promise survival of every device/filesystem power-loss failure, protect against out-of-band edits, or coordinate OneDrive/network filesystems. The storage directory must be local to this one process. DELETE permanently removes the envelope; there is no recycle bin, tombstone or server undo history.
The service retains only the current revision. Pagination can supply the returned revision; if head changes between pages, it returns 409 snapshot_changed so the caller can restart. Project-catalog pagination is live UUID-order pagination; concurrent creates/deletes may change later pages. There is no request receipt/idempotency ledger. Never automatically retry a model-creating request after an uncertain connection failure. Reload the project after an uncertain conditional write; the original ETag cannot overwrite an accepted newer revision.
Back up by stopping the server cleanly and copying committed JSON envelopes to a separate location. Restore into an empty directory, start the service, and check readiness and representative exports. This preserves current models and revisions, not earlier revisions. Do not treat a live directory copy as a consistent multi-project backup. Keep backups outside the managed data directory.
Limits and errors
- Raw request bodies: 8 MiB, including requests without
Content-Length. Encoded/compressed request bodies are rejected. Serialized model JSON and serialized source-bundle JSON each have an 8 MiB cap. The latter is checked again after omitted IDs are inserted. The storage envelope allows both representations plus small framing overhead. - Source bundles contain at most 256 files, with at most 32 include edges along a path and 1,024 include edges overall. Includes resolve only against submitted logical
.arcadiapaths; no server filesystem or network access is involved. All files must be reachable, and cycles, duplicate keys, case collisions and escaping paths are rejected. - At most eight admitted model requests and eight active/waiting blocking workers. Overload returns
503withRetry-After: 1. A 30-second request deadline bounds body receipt and response waiting. A started blocking write can finish after that deadline;504 outcome_unknownmeans reload before retrying. - At most 5,000 elements plus relations, 20,000 total persistent model records, and 50,000 ordered step references per model. Graph requests allow 1–100 roots, depth 0–4, and at most 1,000 returned objects. List pages contain 1–1,000 records, default 100.
- Source declarations and decoded JSON records are counted before structural validation. V1 import includes the occurrences and placements migration will generate in its early budget check. These limits apply to import, source/model replacement, candidate validation and full-model transaction commands. Over-budget candidates cannot monopolize the catalog while being rejected.
- Transactions accept at most 200 commands, including nested batch wrappers. All commands apply to a candidate, then shared validation and authored-source writeback accept or reject the batch atomically. Dry run returns the proposed model, source project and diagnostics without writing.
- The fixed store cap is 128 hosted models and 256 MiB of serialized envelopes. New/imported/replaced models that exceed it return
507 store_capacity. Startup enforces the same limits. Delete/export unneeded projects before adding more data; there are no hidden eviction or pruning rules. The server stores all current models in memory; memory includes Rust object overhead and in-flight candidates in addition to serialized bytes.
Errors use application/problem+json with stable code, HTTP status, human detail, and contextual issues when available. Responses carry X-Request-ID; configured bearer credentials and source/model bodies are not logged. The core currently supplies human command errors; the API maps them to 422 command_rejected without parsing English messages into invented machine codes. Source-project errors preserve logical file paths, UTF-8 byte spans, line/column and related-file locations.
Linux deployment
deployment/Dockerfile builds a headless image from the repository root:
docker build -f deployment/Dockerfile -t arcadia-server:local .
docker run --rm --name arcadia-server \
-p 127.0.0.1:8787:8787 \
-v arcadia-data:/data \
--mount type=bind,src=/absolute/path/arcadia-token,dst=/run/secrets/arcadia-token,readonly \
arcadia-server:local
The token file must be readable by UID 10001 inside the container. The default container binds 0.0.0.0 internally and therefore requires the token. Publishing only to host loopback keeps it behind a host TLS reverse proxy. Keep the volume on local storage; do not run multiple replicas against the same volume.
For a native Linux installation, create an arcadia service account, install the release binary at /usr/local/bin/arcadia-server, put the restricted token file at /etc/arcadia/token, and install arcadia-server.service. The unit's StateDirectory creates /var/lib/arcadia for that account. Inspect the unit and paths before enabling it with systemd. It binds loopback; expose it remotely through a TLS reverse proxy with request-header, connection and idle-time limits. No TLS endpoint or certificate provisioning is built into the binary.
Ctrl+C/SIGTERM stops admission and drains HTTP requests. Already-started blocking tasks run to their completion boundary. The systemd example permits 45 seconds before a supervisor kill; an interrupted process recovers by loading the last installed envelopes. /health/ready means the initial store validation succeeded and the process can respond; it is not an ongoing disk-free-space or integrity probe.
Desktop and automation integration
A desktop or automation client can query current JSON/source, edit with shared typed commands, validate/dry-run, and commit using the original ETag. For hosted editing, keep the user's draft until the service acknowledges the new revision. On 412, preserve the draft and reload server state for review; a blind replacement retry would discard another client's work.
Use POST /api/v1/projects/import-source-project to import {"entry":"model.arcadia","files":{"model.arcadia":"..."}}. Use GET and conditional PUT /api/v1/projects/{id}/source-project to export or edit that complete authored workspace. These routes preserve comments, line endings, entry names and module layout. Missing declaration IDs are inserted once; keep the accepted bundle returned by PUT or subsequent GET. Candidate validation accepts {"format":"source-project","source_project":{...}} and reports diagnostics without adoption.
Typed commands, renames and transitions update the same authored files using command identity correspondence, including UUID swaps and delete/recreate operations. Model JSON and canonical single-file source replacements preserve the existing authored layout using common UUIDs. Canonical /source remains an interchange view; use /source-project to retain or edit file layout and comments. Source writeback failure rejects the whole candidate instead of discarding the authored files. Comment-only edits share the same ETag as semantic edits, preventing one client from overwriting another client's source work.
External tools can use the artifact-link and trace query routes, and create/edit those records in the same atomic transaction as model changes. URI and external revision fields preserve the author's values; the service supplies no fetching, credentials, artifact-content verification or AI execution runtime. OpenAPI is the integration contract. An optional future adapter can call this API without introducing a second mutation path.
POST /projects/{id}/transitions/preview produces a shared core transition plan and base_revision without storing a plan or changing the model. Send that response to /transitions/apply with the original ETag. Apply checks the server revision and the core's model-change fingerprint, preserves planned IDs, validates the candidate, and commits once. Plans with unresolved conflicts cannot apply. A submitted plan is a client-owned command proposal, not a signed server authorization; a client able to apply a plan already has the same write permission as the transaction endpoint.
Verification
cargo fmt -p oa-server -- --check
cargo clippy -p oa-server --all-targets --locked -- -D warnings
cargo test -p oa-server --locked
The HTTP integration suite starts real loopback listeners over isolated temporary stores. It covers competing conditional writes, stale and nonexistent deletion, restart persistence, source/JSON equality, dry-run and no-op behavior, invalid candidates, graph bounds, authentication, body limits, startup corruption/locking, capacity, transition revision checks, artifact persistence and a forced replacement failure. It also checks prompt rejection of over-budget source/native/legacy imports and replacements, catalog responsiveness during candidate preparation, and a stale prepared candidate losing to a newer HTTP edit. A deterministic store test checks the final revision comparison with two fully prepared candidates. Passing those tests does not by itself prove power-loss behavior, external reverse-proxy configuration, container execution or remote deployment. Platform/release results must be recorded only after running their checks.
Verified on 2026-09-05:
- Windows authored-source service extension: nine new real HTTP tests and one OpenAPI contract test pass, covering exact multi-file/comment round trips, comment-only ETags, ID insertion, source/model parity, command provenance through UUID swaps and equal-model delete/recreate, dry runs, transitions, invalid dependencies, concurrent writes, restart migration and forced source-write failure. The source-byte accounting unit passes; the focused
source_regression selection passes all 14 HTTP/schema tests. Strict server all-targets Clippy passes. These are local Windows results; Linux and remote deployment remain separate checks. - Windows R1/R2/R4 source/API integration: all 16 current HTTP/configuration tests plus the deterministic commit test pass; strict server all-targets Clippy passes. New cases cover mission maps, branching/reused chains, typed call value edits, original V2 compatibility, source/JSON equality and persistence after restart. See
harness/release-language-integration.md; native UI and remaining release semantics are separate gates. - Windows R2 release checkpoint: all 14 server integration/configuration tests plus one deterministic commit test pass; formatting and strict all-targets Clippy pass. A separate optimized executable probe rejects the previously stalling 12,000-activity request in 0.050 seconds; concurrent JSON import and accepted-source preparation leave catalog reads responsive. See
harness/release-text-service-audit.mdfor exact measurements and scope. - Ubuntu 24.04 under WSL, prior V2 baseline: all 12 then-existing tests passed using native Linux processes and temporary filesystem stores; strict all-targets Clippy and the optimized build passed with the locked, vendored dependency set. A separate release-executable smoke run verified bearer authentication, non-loopback token enforcement, exclusive directory locking, conditional create/update/delete, update and deletion persistence across restarts, served OpenAPI, and clean SIGTERM shutdown. The new first-release changes still require native Linux re-verification.
systemd-analyze verifypasses after copying the unit to a native temporary directory with mode 0644 and substituting the built executable's path for its documented installation path. The service account, token installation and system-wide service activation were not performed.- A Docker daemon was unavailable, so the Docker image was not built or run. No remote service or TLS proxy was deployed. The declared Rust 1.90 minimum and Docker builder were not independently compiled in this verification; the available stable toolchains built the locked dependencies successfully.