Skip to content
docs
Arxo ↗

Runtime filesystem isolation

For LLMs10 sections

Run the server so it can read exactly the corpus and cases it serves, write exactly the channels the operator opened, and reach exactly the network peers the operator named — with each boundary tied to a real mechanism or marked as operator policy.

Component law-mcp (stdio and --http). Scenario: a deployment host serving one profile. All users, paths, and URLs below are synthetic examples.

BranchCoverage in this article
MCP (law-mcp-server, stdio + HTTP)full recipe: user, mounts, network, temp
law servesame OS mechanisms apply; serve paths (world, journal, token file) are in Deploy a private HTTP service and Configuration reference
  • A checkout root containing the profiles directory and the CLIR tree (real root markers), or an explicit --root / LAW_MCP_ROOT (real).
  • A decision on the service user (no code default; the process runs as whoever starts it) and on which optional legs are enabled (journal socket, OTLP, Lens answers, neural appendix).
  1. Create a dedicated service user (operator-policy-example: useradd --system --no-create-home law-svc). The server needs no root powers: it binds a high port by default (8722, real) and opens no privileged socket itself.
  2. Mount the release read-only (operator-policy-example: mount -o remount,ro /opt/law-dsl, or a read-only container layer). This holds only while the profile denies the writing facets: with editing/drafting allowed, the server writes case packages and author-tree files under the root (see the effects matrix). The explicit --export-neural-input PATH (real, main.rs) takes an operator-chosen path outside the release.
  3. Grant read on the root, write nowhere by default (operator-policy-example modes: root 0755 root:root, service user in no write group). The server reads profiles, the CLIR tree, and case directories (law.toml, law.lock, queries) under the root (real behavior).
  4. Keep case packages inside the root. Real containment (execution.rs): the package path is canonicalized and must start with the canonical root and contain law.toml; symlink escapes are refused with case_ask.outside_root. Do not work around this with symlinks into foreign trees.
  5. Open only the write channels you use (all real): journald socket (datagrams; absent socket falls back to stderr), stderr capture (your supervisor’s), OTLP receiver (LAW_MCP_OTLP_URL, spans only, no bodies), the export path if --export-neural-input is used, plus — when the writing facets are allowed — the case/author trees the tools update. Workbench runs additionally create temp dirs and a request.json under the OS temp dir, so TMPDIR must be writable (no other temp contract exists).
  6. Name the allowed network explicitly. Real client-side rules (service_client.rs, shared by Lens answers and neural appendix): production requires HTTPS; plain HTTP only to loopback (127.0.0.1, localhost, [::1]); zero redirects; 30 s timeout; response caps 4 MiB default, 32 MiB for Lens answers. Credentials in the URL (@, ?, #) are refused. The process otherwise opens no outbound connections: no telemetry, no update checks, no LLM calls.
  7. Treat foreign drafts as untrusted input. Two touch points (real): --export-neural-input PATH hands the search index input to a separate neural service at index-build time only; the law_search neural appendix posts query text + lexical candidate ids to {LAW_NEURAL_URL}/v1/appendix and validates the reply (must carry neural + candidates, at most 8, each re-checked against the local index — reference/search.rs). Anything the service returns outside that contract is discarded with a search.neural_bad.* reason, and the lexical answer stands.
  • Under a profile that denies the writing facets (editing, drafting), the service user can start the server, serve calls, and emit journal/OTLP records, but cannot modify the release or any case input. With those facets allowed, case/author-tree writes and workbench temp files are the tools’ designed effect (see the effects matrix), not an isolation failure.
  • A law_case_ask call whose package climbs out of the root with parent-directory segments (created-example) is refused (outside_root/not_found, real codes); no file outside the root is opened.
  • With all LAW_*_URL variables unset, the process makes zero outbound connections (real: each leg is disabled without its URL).
  • Start read-only and call law_search: neural.active:false with the unset-URL reason (real) proves no network attempt.
  • Set LAW_ANSWERS_URL=http://created-example.invalid (non-loopback plain HTTP): publication tools fail with SERVICE_CONFIG https_required (real), proving the guard runs before any byte is sent.
  • strace -f -e trace=%network (operator-policy-example) during a local-only call shows accept/socket for journald at most, and no connect to foreign hosts.
  • no law-dsl root found above …; pass --root (real): start from inside the checkout or set LAW_MCP_ROOT.
  • bind on "0.0.0.0" without LAW_MCP_TOKEN is refused (real): set a token or confirm a deliberate open bind with LAW_MCP_PUBLIC=1 (real escape hatch).
  • 401 on POST /mcp (real): missing/wrong Authorization: Bearer against LAW_MCP_TOKEN / --token. 403 (real): request Origin not in --allowed-origin / LAW_MCP_ALLOWED_ORIGINS.
  • Read-only root surprises: any write the profile’s facets allow can fail for permission — case/author-tree writes (editing, drafting), workbench temp files under TMPDIR, and --export-neural-input, not just the export flag. Diagnose by asking which tool attempted the write and checking its row in the effects matrix: a denied-by-filesystem write from an allowed facet means the mounts (step 2/5) are narrower than the profile, not that the tool misbehaved.
  • “Isolated” in this article means only: root containment for reads, the write channels step 5 enumerates (nothing wider), allowlisted outbound legs, and no ambient network use. Process sandboxing (namespaces, seccomp, containers) is the operator’s layer — the binary requests none.
  • Immutable releases, the service user, directory modes, and firewall rules are operator policy with reference-setting examples above; the engine’s part is that read-only operation is sufficient only for profiles denying the writing facets — with editing / drafting allowed, the writable trees are part of the contract.
  • Case inputs are trusted once inside the root: filesystem ACLs, not the engine, decide who may change a law.toml.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.