Skip to Content
ArchitectureReasoning serviceHTTP and MCP interfaces
Architecture · Reasoning

One guarded path, for every caller.

Two checks in middleware, then the guards each route applies. The shaded steps decide who is asking and what may leave.

What it is for

The reasoning service is the one place where questions, findingsFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner., operations and notification subscriptions are handled. The web application reaches it over HTTPHTTPHypertext Transfer Protocol: the rules a browser or program uses to send a request to a web service and receive its response. for people working in the browser. MyceliumMyceliumThe application infrastructure underneath NovusLens. Its connected services exchange updates and keep the ordered structural log. Model Context ProtocolMCPModel Context Protocol: a standard way for an AI application to discover and call named tools supplied by another program. (MCP) clients reach the same five tool handlers through the meshMeshA set of connected Mycelium services that exchange updates and keep the NovusLens log. gatewayGatewayA service that accepts requests and passes them into another system. The Mycelium gateway gives NovusLens access to the mesh.. Both paths run the same guards, so a viewer sees the same slice of the organisation whichever way they ask.

How it works

Every HTTP request passes through middleware, _service_boundary in serve.py, before any route runs. It does two things in order.

  1. Size limit. It reads the content-length header. A value above MAX_BODY_BYTES (4 MiBKiB and MiBBinary size units: one KiB is 1,024 bytes; one MiB is 1,048,576 bytes. Evidence limits use these units., 4 * 1024 * 1024 bytes) returns 413. A value that is not an integer returns 400. The check reads the header only (see Invariants and failure behaviour).
  2. Service tokenService tokenA secret value sent with requests between NovusLens services. The receiving service refuses requests with a missing or wrong value.. In the customer profileDeployment profileA setting that selects customer or development behaviour. Customer mode refuses development credentials and uses the signed-in session as the identity. (CONWAY_PROFILE=customer), every path except /health and /ready must carry the header x-conway-service-token. The value is compared with CONWAY_SERVICE_TOKEN using hmac.compare_digest. A missing or wrong token returns 401. In the dev profile this step is skipped.

The web application supplies the header through its helper serviceHeaders() in apps/ui/lib/profile.ts, which sends the token only when one is set. For browser requests, the web application adds the token after taking the viewer from sign-in. Trusted command-line evidence producers also use the service token. That is why the service can trust the viewer named in a request body or query string.

A browser request through the web application proxy, the size limit and service token check, the route handler, the guards, and the database.

Each route then follows the same shape. It calls guards.resolve_viewer, which looks the viewer up in the actor_map table. An unknown viewer raises UnknownViewerError, which the route turns into a 403. guards.require_org limits an actor to their own organisation; the operator can access any organisation. Write routes also call guards.require_writer. The board role is read-only, so require_writer refuses it. Most routes then call guards.audit, which inserts a row into query_audit and commits. Writes go to the mesh through TwinWriter and commits, and then operations._refresh_read_model materialises the read modelRead modelThe database tables used by the map, Ask and Find. NovusLens rebuilds these tables from its history of recorded changes. again, so the response reflects the change.

ScopeScopeThe work, data, activity or access that a rule, statement or calculation covers. For access, it names the records an account can see. For team measures, “Own operations” covers the selected unit. “Including descendants” adds the units beneath it. is applied in SQLSQLStructured Query Language: the language used to read and change records in a database.. guards.scope_cells_sql returns the cellsCellA box on the map that stands for a team, service, supplier or AI agent. It is a unit you can inspect, not necessarily a department or a person. a role may see. The cxo, operator and board roles see every cell. A bu-owner sees the subtreeSubtreeOne unit and every unit nested beneath it in the organisation's hierarchy. under their scope_cell, found with a recursive query. Other roles see the infrastructure cells and the parents that hold them. A question can narrow this further: QUESTION_CELLS is a context variable that every retrieval intersects with the cells the question was scoped to.

Results are filtered before they leave. guards.guard_payload recursively filters string fields for verdict wording. It preserves customer wording in body_md, excerpt, quote, body, document and note_says. filter_verdicts replaces verdict wording with a withheld marker. The response lists withheld wording under _verdicts_withheld. The tool handlers in tools.py return through guard_payload, and so does the evidence route. Compass calls filter_verdicts, then claims_check, in compass.py. The latter withholds a couplingCouplingA link where one team or service depends on another. Its Loose, Buffered or Rigid class describes the time needed for a handoff. class or lead timeLead timeThe elapsed time between starting a request or handoff and completing it. absent from the answer’s tool results.

Limits on model requests

Two routes call the configured model directly: /compass and /strategy. Both wrap their work in _InferenceSlot. A process runs at most INFERENCE_GLOBAL (8) model-bound requests at once, and a single viewer runs at most INFERENCE_PER_VIEWER (2). Beyond either limit the slot raises 429. The group question route, /compass/group, does not take a slot.

Group routing output

Routing selects a published field before the renderer examines member records. A missing snapshot or an unknown comparison winner does not make that field unavailable. Everyday descriptions of map evidence or recorded issues can refer to their published counts, with the interpretation stated. Operational abilities remain unavailable; record counts are not substitutes for them.

For OpenRouter group routing, Compass supplies a static JSONJSONJavaScript Object Notation: a text format for data, using named fields, lists and values. Configuration files and service messages use it. Schema for measure, direction and reason. The first two use the existing output options; all three fields are required and extra fields are excluded. The request asks for strict structure and provider parameter support. The schema does not depend on the question or specify its answer. Parsed output still faces the existing defensive checks, and the public route retains its separate refusal of unsupported abilities.

Callers without a schema and local-model calls keep their existing request format. EndpointEndpointA particular address on a service that accepts a defined kind of request. compatibility varies, so a schema request is not proof of semantic correctness. It does not control reasoning tokens, fix a provider limit or guarantee availability. Real routing checks remain necessary. The live check runs 11 cases twice, for 22 runs. Private INFO diagnostics distinguish missing or unsupported fields from an explicit model abstention. They record fixed states and allowed field values, with presence and type flags. They omit questions, reasons and arbitrary model text. This adds no public response fields and changes no routing decision.

The MCP interface

mcp.py exposes five handlers as storage layer MCP tools. Their schemasSchemaThe declared structure of data: which fields or tables exist and which values they accept. are in SCHEMAS: query_graph, query_meaning, simulate_scenario, retrieve_evidence and highlight. Each is named with the prefix conway_, because the Mycelium’s tool list is shared across providers. register_tools writes each schema to the Mycelium’s MCP registration key, built by keys.mcp_tool_registration (the convention tools/<name>/<node>). serve_invocations then reads JSON-RPCJSON-RPCA message format for asking another program to run a named method and return a result or an error. Messages use JSON. tools/call envelopesEnvelopeThe surrounding fields that identify a message, its sender, its type and its time, alongside the message's data. from agent.rpc_serve("mcp.invoke"). It skips other providers’ tool names. It calls the matching tools.TOOLS function with a fresh database connection and the supplied arguments, then returns JSON. A failure becomes a JSON-RPC error with code -32603, carrying the envelope’s id. The viewer and organisation come in the arguments, as the schema requires. The handlers in tools.py resolve the viewer, apply the organisation binding and scope, and return through guard_payload. The /tools/{name} route calls the same functions over HTTP.

Start-up in main()

main() reads --host (default 127.0.0.1), --port (default 8500), --mesh-host (default 127.0.0.1) and --mesh-port (default 8400). In the customer profile it first calls require_customer_safe for CONWAY_SERVICE_TOKEN and MYCELIUM_GATEWAY_TOKEN, and refuses to start if either is missing or a development value. MCP tools are registered on the gateway only when the process is not in the customer profile, or when NOVUSLENS_MCP_EXPOSE is 1. Registration is attempted up to 30 times, one second apart. If the gateway never answers, the process exits. A daemon thread then runs serve_invocations and restarts it after two seconds whenever it fails. Uvicorn uses --host, default 127.0.0.1. Compose sets 0.0.0.0 inside its private application network.

Interfaces

DirectionPartyHow it is reached
InWeb applicationHTTP to CONWAY_REASONING_URL (default http://127.0.0.1:8500). In the customer profile each call carries x-conway-service-token.
InMycelium Model Context Protocol (MCP) clientsJSON-RPC tools/call relayed by the mesh gateway on the mcp.invoke kind. Registered only when the service is not in the customer profile, or NOVUSLENS_MCP_EXPOSE=1.
InOperator command./lens connectors document shares ingest_document with the HTTP route, so it needs no service-token header.
OutPostgresPostgresThe database software that stores the tables read by the NovusLens screens, along with document bodies, access records and protected personnel records.conway_graph.db.connect(). Audit rows go to query_audit.
OutMesh gatewayTwinWriter and sdk_agent at the MESH host and port, for commits, materialisation and MCP registration.
OutLocal modelLanguage modelSoftware that produces text from instructions and supplied context. NovusLens checks its answers against the records retrieved for the question. runtimeRuntimeThe software that runs a model or agent. A model runtime answers model requests; an agent runtime carries out the customer's agent actions.Compass and Strategy. A failed call surfaces as a URLError and becomes a 503.
OutWebhookWebhookA web address that receives a message automatically when an event occurs. NovusLens notifications send record identifiers there. receiversNotification delivery, to the destinations an operator subscribed.

The full route table is on the HTTP endpoints page. The reasoning overview describes what the service depends on and how it starts.

VariableDefaultEffect
CONWAY_PROFILEdevcustomer turns the development conveniences into refusals.
CONWAY_SERVICE_TOKENnoneThe shared token for the web application hop. Required in the customer profile.
MYCELIUM_GATEWAY_TOKENnoneChecked at start-up in the customer profile.
NOVUSLENS_MCP_EXPOSEunset1 registers the MCP tools on the gateway in the customer profile.
NOVUSLENS_MESH_HTTP_ADDR, NOVUSLENS_MESH_HTTP_BASE127.0.0.1, 8400Default mesh host and port. --mesh-host and --mesh-port override them.
NOVUSLENS_WEBHOOK_ALLOWLISTunsetA comma-separated list of host suffixes. When set, a notification destination must match one of them.
NOVUSLENS_ALLOW_LOOPBACK_WEBHOOKunset1 allows a loopbackLoopbackA network address that refers to the same host, such as 127.0.0.1. Other hosts cannot reach a service through that address. notification destination.
CONWAY_REASONING_URLhttp://127.0.0.1:8500Read by the web application, not by this service.

Operations

Three operations change state through the same guards. Each refuses to act on its own.

  • Propose. propose_org runs the interview or checks an upload and returns a proposal. Nothing is committed. It refuses an organisation the operator has forgotten, so an import fails before review, not at commit. A transcript for an organisation that already exists runs seeded, so it annotates the existing structure rather than replacing it.
  • Commit. commit_org lands a confirmed proposal as structural commits and artifactArtifactA stored source file or evidence body that a record can cite. Keeping the exact content lets a reader check what supported the record. requests, then re-materialises. It refuses an organisation the operator has forgotten. An org-chart proposal seeds a new twinTwinNovusLens’s model of one organisation, built from records about its teams, services and how they rely on one another. It is only as current as those records. and never reseeds an existing one.
  • Fulfil. fulfil_request answers an open artifact request with a document. It dispatches on the artifact kind. A document is stored with no rungRungOne of three levels of support for a recorded value: told, shown or measured. The level appears beside the value. claimed unless a parser substantiates it.

Resolution and retractionRetractionThe withdrawal of a finding or meaning entry, with the reason recorded. The record keeps who withdrew it, when and why; it is not deleted. have their own routes. may_resolve and may_retract decide whether the viewer is the accountable ownerAccountable ownerThe person recorded as answerable for a team, service or agent. NovusLens does not assume that this person is also the one who runs it day to day., the steward or the speaker of a finding.

Notifications

notify.py tells a person that a recorded finding exists. It does not carry the claim itself, and it offers no action.

  • Subscribing and polling are operator-only. _operator refuses any other role.
  • Only the webhook channel is implemented, and the endpoint must be http or https.
  • Subscriptions may name only the kinds in NOTIFIABLE. Scope is checked when the subscription is created and again at each delivery.
  • _check_destination refuses cloud metadata hosts, .internal names and link-local addresses. It refuses loopback unless NOVUSLENS_ALLOW_LOOPBACK_WEBHOOK=1, and it applies NOVUSLENS_WEBHOOK_ALLOWLIST when that is set.
  • Each notice identifies the organisation, finding, kind, status, cell, revision, reason and subscription. It links to the organisation and, where present, the cell. It does not carry the finding’s text.
  • Delivery does not follow redirects, uses a 10-second timeout, and records each attempt as issued or failed, with the HTTP status or the exception name.

Invariants and failure behaviour

  • Unknown viewers get nothing. A viewer missing from actor_map is refused on every route that resolves one. There is no default-open path.
  • Organisation binding. An actor acts only in their own organisation.
  • Board cannot modify organisational claims. Those write paths call require_writer. Archiving assessment inputs is a separate action explicitly allowed for board accounts.
  • Disputes are entries, not edits. /dispute writes a finding and a provenanceProvenanceThe record of where a value came from, who stated it and when. Every value keeps this record. marker to the log. The response says the divergence is retained.
  • Body size. A request whose content-length header exceeds 4 MiB gets 413, in both profiles.
  • Token. In the customer profile, a request without a matching token gets 401. /health and /ready are exempt, and they report only liveness and projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. age.
  • Mapped failures. Four exceptions return 503 with an explanation. TimeoutError means Mycelium did not answer. LogDivergedError means log and read model disagree, so nothing is shown. URLError means the model is unreachable; records and findings remain unaffected. psycopg.Error means the database is unreachable and nothing changed.
  • Model request limit reached. A 429 is returned when the global or per-viewer slot is full. The request is not queued.
  • Health. /health returns 503 with a list of problems when the database or the mesh gateway does not answer. /ready reports the age of the last projection pass and returns 503 when the read model is stale or failed.
  • MCP failures. A tool error becomes a JSON-RPC error and does not hang the caller. The provider loop restarts after each failure.
Last updated