118 lines
18 KiB
Markdown
118 lines
18 KiB
Markdown
# 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](api.rst); machine-readable schemas are in [openapi.json](openapi.json), also served at `/api/v1/openapi.json`. The original [service research](research-v2-service.md) 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:
|
||
|
||
```powershell
|
||
cargo build --release --locked -p oa-server
|
||
.\target\release\arcadia-server.exe
|
||
```
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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](../deployment/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
|
||
|
||
```sh
|
||
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.
|