Skip to content
docs
Arxo ↗

Tool profiles and input policy

For LLMs15 sections

Show how a corpus profile narrows the tool surface (facets), what a caller sees when invoking a forbidden or unknown tool, which inputs carry rights and which do not, what side effects tools have, and where fact authorship is and is not verified.

Component law-mcp-server (law-mcp/): tool registry data/tools.json (36 tools) plus science tools (src/science.rs), facet gating (src/server.rs, src/profile.rs), fact schema (law_ask.facts, law_case_*). Scenario: operating one profiled slice (e.g. islam on /mcp/islam). All calls below are synthetic created-examples; tool names, facet names, error codes, and schema fields are real.

  • A profile file <name>.json in the checkout’s profiles directory, or all.
  • A running instance started with --profile <name> (facets bind at startup; changing the file needs a restart).
  • A token or loopback access (see Authentication and access control).

Every tool carries one facet. A profile’s facets.allow lists the served facets; facets.deny maps each withheld facet to a non-empty human reason. Validation (validate_facets) is strict: each of the 9 known facets must be named exactly once — in allow or deny, never both, never neither; duplicates and empty reasons refuse startup. Profile all allows all 9 with no reasons.

FacetTools (from tools.json, contract order)Typical effect
referencelaw_search, law_passport, law_answer_capabilities, law_packages, law_rules, law_sources, law_queryread-only: find and quote formalized law
executionlaw_capture_answer, law_prepare_answer, law_publish_answer, law_revoke_answer, law_ask, law_screen, law_explain, law_inspect, law_case_askevaluate; prepare/publish/revoke frozen answer documents
draftinglaw_check, law_pin, law_draft_check, law_draft_eval, law_measure, law_draft_verbalize, law_draft_differential, law_draft_impactauthor workbench: check drafts, pin sources, measure completeness
editinglaw_case_propose, law_case_facts, law_case_decide, law_case_document, law_case_checkcase-package journal: propose/accept facts, write documents
argumentationlaw_argueread-only: dispute map over a derivation
adversariallaw_loophole, law_amendcircumvention analysis; amendment drafting
processlaw_process, law_process_runread-only: procedure graph; replay a case journal
editionslaw_editionsread-only: what changed between two law-dates
feedbacklaw_reportappend a formalization-divergence report to the server journal

Real example — the shipped islam profile allows 6 facets and denies 3 with reasons (drafting: “author workbench … is not executed on the public slice”; adversarial: doctrine “is not amended”; editing: “executed only on the author’s local slice”). Copy that shape for a new slice; the reason strings are operator-chosen text.

  1. Write the profile: allow the facets the slice needs, deny the rest with a reason each (all 9 named exactly once).
  2. Start with --profile <name> and read the stderr launch line for the profile name.
  3. Call tools/list (real method, synthetic transport): the reply contains only allowed-facet tools, with the facet field stripped. Count them against the table above.
  4. Call one denied tool directly (next section) and confirm -32002.

tools/list filtering is presentation; enforcement happens per call in call_tool. A direct tools/call naming a denied-facet tool returns a JSON-RPC error (real codes, synthetic ids):

JSON
{"jsonrpc": "2.0", "id": 7, "error": {"code": -32002, "message": "tool 'law_pin' ... facet 'drafting' ... <deny reason>"}}

Error-code table (all verified in server.rs):

CodeMeaningWhen
-32002facet deniedtool exists but its facet is not in facets_allow; message quotes the profile’s deny reason
-32602unknown toolname matches no registry entry
-32601unsupported methodmethod is not initialize/tools/list/tools/call/prompts/.../ping/notifications
-32001 + CALL_TIMEOUTcall overran LAW_MCP_CALL_TIMEOUTcomputation abandoned; result discarded; overrunCalls in /healthz counts it
-32603 + INTERNALhandler panickednever a legal outcome; journal records MCP_TRACEBACK

Input-predicate rights: none per predicate

Section titled “Input-predicate rights: none per predicate”

law_ask (and law_argue, law_process_run, law_case_ask) accept facts shaped {predicate, args, negated?, package?, provenance?, judgment?}. There is no per-predicate allow-list, no per-fact role, and no caller-rights check on inputs: any caller passing the instance check (see Authentication) may assert any predicate with any arguments. What the server does instead:

  • package on a fact disambiguates a short predicate name; it grants nothing.
  • Unknown predicates and unsatisfied premises surface as “not established” with a whyNot report — a logical outcome, not a refusal.
  • law_screen builds only a superset pre-filter: every passing row must still be fully evaluated with a proof graph; the screen grants no shortcut around evaluation.

Fact actions are call-scoped: facts live for one evaluation and are never stored, except inside the call journal record (capped by LAW_MCP_LOG_MAX_BODY) and inside caller-built case packages via the editing facet.

This table is the single source of truth for what each tool class reads, writes, sends out, and requires for affinity. Other articles defer to it; if they seem to disagree, this table wins and the other article is a bug — report it.

Effect classToolsTrigger / settingReads / writes / sendsSame instance?Re-creatable after loss?
Pure evaluationlaw_ask, law_argue, law_explain, law_inspect, law_editions, law_process, law_process_run, law_query, law_screen, reference readersfacet allowedreads corpus + case input; writes nothing (journal/OTLP observe, never alter answers)no — any identical instancen/a (no state)
Answer publicationlaw_capture_answer, law_prepare_answer (24 h private freeze), law_publish_answer (public URL + separate revoke secret), law_revoke_answerLAW_ANSWERS_URL and LAW_ANSWERS_TOKEN both set (read per call); else the tools answer “unavailable”no local writes — the call is proxied to the external answers service, which owns the freeze, the public URL, and the revokeno for the call itself; the frozen document lives in the answers service, not on any instanceonly from the answers service
Neural appendixlaw_searchLAW_NEURAL_URL set (token required by the client; read per call); else lexical-only answersends the query text to the external neural service; writes nothing locallynon/a (no state)
Case-package journallaw_case_propose, law_case_facts, law_case_decide, law_case_document, law_case_checkediting facet allowedwrites the case package on disk (one transaction) — the reason public slices deny editingonly if instances share the tree; otherwise the write lands on one instance’s filesonly from backup
Draft checks and pinslaw_check, law_pin, law_draft_*, law_measuredrafting facet allowedwrites working files/pins of the author tree — the reason public slices deny draftingsame-tree requirement as aboveonly from backup / VCS
Saved-answer explainlaw_explain with a law://answer/ URIartifact file presentreads the artifact from LAW_MCP_ARTIFACT_DIR (default $HOME/.cache/law-dsl/mcp-answers/<profile>); no tool in this server writes there — artifacts must arrive out-of-band (shared mount, copy step)yes, unless the directory is shared: the reading instance must see the fileonly from whoever saved it
Divergence reportslaw_reportfeedback facet allowedserver journal only; changes no law and no answernon/a (journaled)
Always-on side channelsevery calljournal enabled (default) / LAW_MCP_OTLP_URL setcall journal to the journald socket or stderr (never to files); OTLP spans outn/an/a
Temp filesworkbench runs—temp dirs + request.json under the OS temp dir; --export-neural-input writes its target filen/a (per-call temp)n/a

Consequences used by the other articles:

  • “The server writes no files” is true only with editing and drafting denied, no export flags, and temp files excepted. Anything else, and the matrix row above names the writer.
  • “Nothing leaves the machine” additionally requires every *_URL leg unset — no listening socket does not stop outbound calls.
  • Scale-out is affinity-free only for pure evaluation plus the two remote legs. Case writes need a shared tree (or sticky routing); law://answer/ reads need a shared artifact dir (or the owning instance).

Initiator identity and author fields: caller-asserted

Section titled “Initiator identity and author fields: caller-asserted”

Fact provenance carries origin (default case_input), evidence (a StableId), span (page/quote/offset), extractor (name, version?, confidence?), and timestamps. All of it is caller-asserted metadata: the server records it into was_generated_by / was_derived_from edges and may warn (FACT_WITHOUT_EVIDENCE when an extractor has no evidence), but it verifies no signature, no author identity, and no extractor claim. Journal field MCP_CLIENT (from X-Forwarded-For/socket) is likewise observability, not authentication (see Authentication and access control). Treat extractor.name as a label the caller chose, never as proof of who produced the fact.

Operational tool profile vs model composition

Section titled “Operational tool profile vs model composition”
  • The operational tool profile is the server’s facet set, bound at startup by --profile. It is the enforcement point: hidden tools stay callable-shaped but answer -32002, and tools/list is its projection.
  • Model composition is whatever tool subset the agent harness exposes to the model (system prompt, tool filter, per-task allow-list). It is convenience and cost control, not a boundary: a model that smuggles a tools/call for a composed-away tool succeeds whenever the server profile allows the facet.
  • Rule: put every real boundary in the server profile (facets + instance token + separate processes per tenant); use model composition only for focus. Audits check tools/list and one direct -32002 probe, never the harness config alone.

On a slice denying drafting/adversarial/editing (the islam shape): tools/list shows 6 facets’ tools; law_ask evaluates; law_pin answers -32002 quoting the deny reason; law_nope answers -32602.

Terminal
curl -s http://127.0.0.1:8725/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| python3 -c "import json,sys; print(sorted({t['name'] for t in json.load(sys.stdin)['result']['tools']}))"
curl -s http://127.0.0.1:8725/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"law_pin","arguments":{}}}'

Success: the name list contains no drafting/adversarial/editing tool, and the second call returns code -32002. (Created-example port; methods and codes are real.)

SymptomCauseFix
Startup: each known facet must be named exactly oncefacet missing or in both listsname all 9 exactly once
Startup: facets.deny reason must be non-emptyblank reasonwrite a reason per denied facet
Startup: corpus profile "x" not found--profile typomatch the profile file name (<name>.json)
-32002 for a tool the slice should servefacet denied in the profilemove it to allow, restart
-32602 for a real tool nametypo in name (e.g. law-ask)use the exact law_* name
Denied tool still listed after profile editprocess not restartedrestart; facets bind at startup
FACT_WITHOUT_EVIDENCE warningfact has extractor but no evidenceattach provenance.evidence or drop the extractor claim
  • Facets are the finest tool boundary the server supports: there is no per-tool flag, per-predicate right, or per-user tool set. Separate processes give instance isolation (own token, own profile, own files), not finer authorization — one execution facet still bundles evaluation with answer preparation and publication in every process. “Evaluate but never publish” has no server-side switch: the nearest real mechanism is leaving the answers leg unwired (LAW_ANSWERS_URL unset), which turns all four publication tools into “unavailable” on that instance.
  • Deny reasons are human text in the profile; they are quoted in -32002 messages and must not contain secrets.
  • extractor/origin fields are unverified labels, not authorship proof. Provenance that must be trusted needs an operator-side signing/inventory process this server does not provide.
  • Science tools additionally require the profile to read calc-multiple-testing; on other profiles they are hidden from tools/list even when their facet is allowed.

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

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