# Configuration reference ## Goal Give every setting of the two network executables in one place: each setting's type, default, source (flag or environment), precedence, validation, whether a restart is needed, and whether it is secret — with links to the scenario articles that use it. ## Scope Components: `law-mcp-server` (binary name in `law-mcp/Cargo.toml`; its help text prints `law-mcp`) and `law serve` (`law-bin-core/src/serve.rs`, `law-serve-core/`). Scenario: any deployment that starts these processes. All values below are synthetic examples. Words like default, implementation-limit, reference-setting, and operator-policy-example are used with distinct meanings (see the last table); implemented / described / verified / supported follow the [section vocabulary](/operate/verification-support-matrix/#section-vocabulary-binding). ## Prerequisites - A checkout or release tree containing the binaries. - For `law-mcp-server`: a corpus profile name (one JSON file in the checkout's profiles directory, or `all`). - For `law serve`: a world directory and a journal directory (see [Deploy a private HTTP service](/operate/private-http-service/)). ## `law-mcp-server` settings Source rule (verified in `src/main.rs`, `Args::parse`): every setting first reads its environment variable, then a command-line flag overwrites it. Precedence is therefore **flag > environment > built-in default** for settings that have both; settings with only an environment source use **environment > built-in default**. All settings take effect only after a process restart: **every change needs a restart**. The four external-leg variables are read from the process environment on every call, but that is not live reload — editing the parent shell or the systemd `EnvironmentFile` does not change the environment of the already-running process, so a new value still needs a restart (or relaunch) to take effect. There is no configuration file and no reload signal. | Setting | Type | Default | Source | Validation | Restart | Secret | |---|---|---|---|---|---|---| | Root tree | path | auto-detected (`find_root` from cwd) | `LAW_MCP_ROOT`, `--root` | must hold the profiles directory for named profiles | yes | no | | Profile | string | `all` (empty env counts as unset) | `LAW_MCP_PROFILE`, `--profile` | `all`, or an existing profile file whose facets name each known facet exactly once | yes | no | | HTTP mode | bool | off (stdio JSON-RPC on stdin/stdout) | `--http` only | — | yes | no | | Host | string | `127.0.0.1` | `LAW_MCP_HOST`, `--host` | non-loopback without a token is refused unless public (exact error names `LAW_MCP_TOKEN` and `LAW_MCP_PUBLIC=1`) | yes | no | | Port | u16 | `8722` | `LAW_MCP_PORT`, `--port` | must parse as u16 | yes | no | | Token | string | none | `LAW_MCP_TOKEN`, `--token` | exact `Authorization: Bearer ` match per request; flag wins over env; empty env counts as unset but empty `--token ""` is a real empty token (quirk: it also permits a public bind while demanding `Authorization: Bearer ` with a trailing space) | yes | **yes** | | Public | bool | off | `LAW_MCP_PUBLIC=1`, `--public` | explicit consent to serve without a token | yes | no | | Allowed origins | comma list / repeatable | empty (any `Origin` header is refused with 403) | `LAW_MCP_ALLOWED_ORIGINS`, `--allowed-origin` | exact string match against the `Origin` header | yes | no | | Call timeout | seconds, fractions allowed | none (no limit) | `LAW_MCP_CALL_TIMEOUT` only | non-negative number; empty or `0` means no limit | yes | no | | Journal on/off | bool | on | `LAW_MCP_LOG=0` disables | — | yes | no | | Journal body cap | bytes per side | `65536` (64 KiB) | `LAW_MCP_LOG_MAX_BODY` only | must parse as usize; `0` means no cap | yes | no | | Journal socket | path | `/run/systemd/journal/socket` | `LAW_MCP_LOG_SOCKET` only | if the socket is missing, records fall back to one JSON line on stderr | yes | no | | OTLP receiver | HTTP(S) URL | none (disabled) | `LAW_MCP_OTLP_URL` only | non-empty string; spans POST as JSON to exactly this URL | yes | no | | Answers service URL | HTTPS URL | none (the 4 publication tools answer "unavailable") | `LAW_ANSWERS_URL` only; stdio + HTTP; read per call | non-empty; http loopback-only; credentials-in-URL rejected | yes | no | | Answers service token | string | none (same unavailability) | `LAW_ANSWERS_TOKEN` only; stdio + HTTP; read per call | required non-empty together with the URL; sent as `Authorization: Bearer` | yes | **yes** | | Neural service URL | HTTPS URL | none (lexical-only answers) | `LAW_NEURAL_URL` only; stdio + HTTP; read per call | same URL rules as above | yes | no | | Neural service token | string | `""` (appendix inactive) | `LAW_NEURAL_TOKEN` only; stdio + HTTP; read per call | empty refused by the client (`SERVICE_CONFIG` → inactive appendix); sent as `Authorization: Bearer` | yes | **yes** | | Artifact dir | path | `$HOME` (required at call time) | `LAW_MCP_ARTIFACT_DIR` only | missing dir and missing `$HOME` fail the call with `ANSWER_RESOURCE_UNAVAILABLE` | yes | no | Two unit templates (reference-settings, not code defaults) — never mix them: - Private (the model for every closed deployment in this section): `kit/secure-deploy/law-mcp-private@.service` sets `LAW_MCP_PORT=%i`, `LAW_MCP_HOST=127.0.0.1`, `LAW_MCP_CALL_TIMEOUT=110`, `LAW_MCP_LOG=1`, `MemoryMax=6G`, `User=arxo-mcp`, `NoNewPrivileges=true`, `RestartSec=3`, and reads the token from `EnvironmentFile=/etc/arxo/mcp-%i.env`. It contains no `PUBLIC` line: without a usable token the process refuses to serve instead of opening up. - Public (deliberate open bind only): the shipped `arxo-ops/deploy/mcp/law-mcp@.service` adds `LAW_MCP_PUBLIC=1`. Copying that line into a private instance silently converts a missing token from a startup refusal into an open evaluator — keep it out of every closed recipe. Flags that are commands, not settings: `--healthcheck` (probes `127.0.0.1:/healthz` with a 4 s timeout, exit 0/2; honors `--port`), `--export-neural-input PATH` (writes the search index document and exits), `-h/--help`. Implementation-limits (constants, not settings): request body cap `MAX_BODY` = 1 MiB (`src/http.rs`); no request batching; no SSE (`GET /mcp` → 405); no sessions (`DELETE` → 405). ## `law serve` settings Source rule (verified in `serve.rs` `parse` + `service.rs` `Config::default`): **flags only** — `law serve` reads no environment variable for its own configuration. All flags are parsed once at startup: **every change needs a process restart**. | Setting | Flag | Type | Default | Validation | Restart | Secret | |---|---|---|---|---|---|---| | World | `--world` (required) | dir or pinned `profile.json` | none | dir, or a file named `profile.json` under `deps/profiles///` matching the lock; a `.arxo` bundle is refused — restore the world dir from the bundle first | yes | no | | Journal | `--journal` / `--no-journal` | dir / bool | none — one of the two is required | `--no-journal` is development only (unrecorded decisions never replay) | yes | no | | Watch world | `--watch-world` | bool | off | watches the `--world` symlink target, warms the new world; the journal stays shared, new records carry the new pin | yes | no | | Host | `--host` | string | `127.0.0.1` | non-loopback without a token is refused unless `--public` | yes | no | | Port | `--port` | u16 | `8480` | must parse | yes | no | | Token | `--token` / `--token-file` (mutually exclusive) | string / path | none | exact `Authorization: Bearer ` match; empty file token refused | yes | **yes** | | Public | `--public` | bool | off | explicit consent to bind without a token on any address; requires the process-worker path (the CLI always sets it to its own executable, `serve.rs`) | yes | no | | Workers | `--workers` | usize | `2` | at least 1 | yes | no | | Queue | `--queue` | usize | `64` | at least 1 | yes | no | | Max body | `--max-body` | bytes | `1048576` (1 MiB) | must parse | yes | no | | Max assertions | `--max-assertions` | usize | `10000` | must parse | yes | no | | Max response | `--max-response` | bytes | `16777216` (16 MiB) | must parse | yes | no | | Call timeout | `--call-timeout-ms` | ms | `30000` (30 s) | positive number | yes | no | | Journald socket | `--log-socket` | path | none (JSON to stderr) | call fields to native journald when set | yes | no | | OTLP receiver | `--otlp-url` | HTTP URL | none (disabled) | must start with `http://` or `https://` | yes | no | Implementation-limits (no flag): socket I/O timeout 10 s (`io_timeout`), journal segment cap 64 MiB (`max_segment`), `Idempotency-Key` 1–255 printable ASCII characters without spaces. ## The `law` launcher The `./law` shell script reads one variable: `LAW_PYTHON` — path to a Python ≥ 3.12 with `jsonschema` and `fastjsonschema` for `law dev` subcommands. It is launcher-only: neither Rust executable reads it. ## Steps 1. Pick the scenario article first ([Deploy a private HTTP service](/operate/private-http-service/) for `law serve`; [HTTP, TLS and reverse proxies](/operate/http-tls-reverse-proxies/) for the MCP edge): copy its exact command, not the tables above. 2. Set secrets through the file/env channel the scenario names (`--token-file`, `LAW_MCP_TOKEN` via a systemd `EnvironmentFile`), never on a shared command line. 3. Start the process and read the stderr launch line: `law-mcp-server` prints address, profile, journal channel, call limit, and OTLP receiver; that line is the effective configuration. 4. Change any setting by editing the unit/flags and restarting the process. ## Expected result - `law-mcp-server --http --port 8722` on loopback without a token starts and answers `GET /healthz` with `{"status":"ok",...}` (HTTP 200). - `law serve --world --journal ` starts after all workers warm up; `GET /readyz` returns 200. - A non-loopback bind without a token exits before listening with the refusal quoted above (exit code 2). ## Result check ```bash law-mcp-server --healthcheck --port 8722; echo "exit=$?" curl -s http://127.0.0.1:8722/healthz curl -s http://127.0.0.1:8480/readyz -o /dev/null -w "%{http_code}\n" ``` Success: exit `0`, a body containing `"status":"ok"`, and `200` for `/readyz`. These commands are created-examples (paths and ports are synthetic); the flags, routes, and fields they use exist in code. ## Failures and diagnostics | Symptom | Cause | Fix | |---|---|---| | `bind on "0.0.0.0" without LAW_MCP_TOKEN is refused` | non-loopback HTTP without token/public | bind loopback, or set a token, or set `LAW_MCP_PUBLIC=1` deliberately | | `address "0.0.0.0" without a token is refused` | same for `law serve` | `--token`/`--token-file`, or deliberate `--public` | | `LAW_MCP_PORT: ...` / `--port: ...` | unparsable port | pass a 0–65535 integer | | `facets.allow contains duplicates` / `each known facet must be named exactly once` | profile names a facet twice or misses one | fix the profile file (see [Tool profiles](/operate/tool-profiles-input-policy/)) | | `OTLP URL: expected http:// or https://` | bad `law serve --otlp-url` | pass a full HTTP(S) URL | | `--token and --token-file cannot be combined` | both passed | keep one | | `direct launch requires worker_exe` | library misuse, not CLI | start via the `law serve` command | ## Support boundaries - Changing any setting without restarting the process is not supported. The four external-leg variables are re-read per call from the process environment, but a changed shell or `EnvironmentFile` value still reaches the process only through a restart (or relaunch); everything else binds at startup. - There is no configuration file format: any `.toml`/`.yaml` snippet for these executables is an operator-policy-example, not a server-read file. - The verified constants with no setting are the 1 MiB body cap, the 10 s I/O timeout, and the 64 MiB segment: tuning them requires a code change. Anything else absent from these tables is either a constant the author did not verify (treat as unsupported until checked) or a new setting — not a hidden option you can guess. ## Vocabulary used in this series | Word | Meaning | Example | |---|---|---| | Default | value the code uses when the operator sets nothing | port `8722` | | Implementation-limit | constant behavior with no setting | 1 MiB body cap | | Reference-setting | value from a shipped deploy file, still overrideable | `LAW_MCP_CALL_TIMEOUT=110` in `law-mcp-private@.service` | | Operator-policy-example | this documentation's suggestion, not read by code | run as a dedicated OS user | | Created-example | synthetic value (path, token, host) safe to copy structurally | `LAW_MCP_TOKEN=REDACTED_EXAMPLE_TOKEN` | ## Next step - To expose MCP over HTTPS: [HTTP, TLS, reverse proxies](/operate/http-tls-reverse-proxies/). - To choose token vs public: [Authentication and access control](/operate/authentication-access-control/). - To narrow the tool surface per profile: [Tool profiles and input policy](/operate/tool-profiles-input-policy/).