Files
ARCADIA-Studio/docs/server.md
T

22 KiB
Raw Blame History

Arcadia Studio server

arcadia-server is a headless Rust REST service for the same schema-5 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 .arcadia paths; 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 503 with Retry-After: 1. A 30-second request deadline bounds body receipt and response waiting. A started blocking write can finish after that deadline; 504 outcome_unknown means 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.

Edit hosted models in the desktop

Start a local service from the repository root, then leave it running while using the desktop:

cargo run --locked -p oa-server --bin arcadia-server

The same command works in PowerShell and a Linux shell and uses the platform-local data directory described above. For a custom store, append -- --data-dir PATH; choose a local directory outside a synchronized folder. For an installed release, run .\arcadia-server.exe --data-dir PATH on Windows or ./arcadia-server --data-dir PATH on Linux.

  1. In the desktop top bar, choose Hosted models.... Enter http://127.0.0.1:8787 as Service address, or your deployed service's HTTPS address. Use the service root, without /api/v1; a reverse-proxy path prefix is supported.
  2. Enter the configured secret in Bearer token if required, then choose Connect / refresh. Tokens stay in application memory; they are not saved in model/source files. Leave the token empty for an unauthenticated loopback service.
  3. To share the current local model, choose Publish as new. This uploads the complete authored source project, including its entry file, included files and comments, and creates a separate service resource. After acknowledgement, the current project's Save destination becomes that hosted resource; an existing local file is not overwritten.
  4. To work on an existing resource, search the Hosted model catalog, select a model, and choose Open selected. More models loads another catalog page. Unsaved local work uses the normal save/discard/cancel prompt before opening.
  5. Edit through diagrams, properties or Textual model. Choose Save to server, or press Ctrl+S, to validate/apply drafts and send the complete source project. The header shows the last acknowledged hosted revision and whether local changes remain. Edits and Undo/Redo stay local until an explicit Save.

An invalid source or property draft prevents publication/save and remains available to correct or revert. A rejected server write or failed connection retains the local model and source history. The application does not silently publish the last valid model while dropping an invalid draft. Detached source drafts must be reattached or reverted before hosted Save.

Review a newer server version

An API client or another desktop can edit the same resource. Each Save includes the exact ETag of the desktop's last acknowledged revision. If the server has changed, it rejects that stale Save with 412 revision_conflict; the desktop retains local work and checks the latest server version for review. Choose Check server to perform the same read explicitly, or retry the check after restoring a failed connection.

The review lists changed, added and removed source files. View server text opens a read-only file; Back to review returns to the list. Reading the server does not replace local content. Reload server version adopts the reviewed bundle as one undoable local change after checking unapplied drafts. Undo can restore the previous accepted local version, but it does not undo the server's revision. There is no automatic merge: inspect and reconcile the intended edits before saving again.

For a connection failure with an unknown write outcome, use Check previous write when offered before another Save. If the server holds exactly the submitted bundle, the desktop acknowledges that save. If it differs, review it. An uncertain publication may already have created a resource; refresh and inspect the catalog before choosing Publish another copy.

Save locally and manage the connection

Choose Save as local... in Hosted models, or Ctrl+Shift+S, and select an .arcadia entry file to retain included files and comments. A successful source-project save switches future Ctrl+S operations to that local destination and leaves the hosted resource unchanged. Choosing JSON exports the model only and keeps the hosted Save destination and unsaved state. Canceling or failing the local save also keeps the hosted destination. Forget connection is available after leaving the hosted session and clears the remembered connection/token in memory.

The desktop accepts HTTPS service URLs and HTTP only for localhost or a loopback IP address. Credentials, query parameters and fragments in the URL are rejected; put the bearer token in its own field. Redirects are not followed. For remote access, configure a TLS reverse proxy as described above; the server itself does not provide TLS. To replace an expired token, enter the new value and choose Connect / refresh for the same service, then retry the check or Save. Connection settings and tokens are not persisted across application restarts.

Automation integration

Automation clients use the same typed model, validation, authored source and revision preconditions as the desktop. Preserve the submitted draft and original ETag until a write is acknowledged; on 412, read and reconcile the current version instead of blindly replacing it.

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.md for 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 verify passes 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.