Skip to Content
ArchitectureDeployment and gatesShared Python: profile, federation, CPL
Architecture · Operations

Six shared modules, each holding one rule.

What it is for

packages/common_py is the Python package that the NovusLens services import in common. It provides shared checks for customer configuration, signed documents, critical path lengthCPLCritical path length. The total coordination weight of the longest chain of dependencies in the model. NovusLens reports it as index points, not elapsed time. (CPL), checks for security problems in software packages, findingFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner. visibility and agent evidence.

These rules shape the mapMapThe view of teams, services and their dependencies for the selected part of the organisation. Details appear beside the map.’s CPL figure, finding visibility, configuration refusals and audit results in test transcripts. The modules themselves are rarely visible to a reader, which is why this page explains each one.

Each module answers one question for every service that imports it. The imports shown are examples, not a full list.

How it works

The profile switch

common_py.profile reads one environment variableEnvironment variableA named setting made available to a running program. The customer launcher reads these settings from the installation environment file., CONWAY_PROFILE. The value is customer when it is set to that word, ignoring case and surrounding spaces. Any other value, including an unset or blank variable, gives dev. is_customer() is the test that every service calls.

Python defaults to dev. Compose explicitly sets CONWAY_PROFILE=customer for the installation. ./lens doctor reports a different profile as a failed installation check.

Four secrets are checked 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.. CUSTOMER_SECRETS names each one with the development value it falls back to in development:

VariableDevelopment valueWhat the customer profile requires
CONWAY_SESSION_SECRETnovuslens-dev-session-secretSet, not the development value, at least 24 characters
CONWAY_IDENTITY_SALTconway-dev-saltSet, not the development value, at least 24 characters
CONWAY_SERVICE_TOKENempty (development runs without one)Set, at least 24 characters
MYCELIUM_GATEWAY_TOKENempty (development gatewaysGatewayA service that accepts requests and passes them into another system. The Mycelium gateway gives NovusLens access to the mesh. are open)Set, at least 24 characters

secret_problems(names=None) returns a list of plain sentences, one for each failing variable, and an empty list when every named secret passes. A variable can produce one of three sentences: NAME is unset, NAME is the PUBLIC development value, or NAME is shorter than 24 characters. Each variable reports only its first failure. With no argument it checks all four.

require_customer_safe(component, names=None) does nothing in development. In the customer profile it raises SystemExit with the component name and every problem, and it tells the operator to set the values in .novuslens.env. The module’s own message says that the platform never invents a customer secret. The callers are:

  • the reasoning service’s start-up, which checks CONWAY_SERVICE_TOKEN and MYCELIUM_GATEWAY_TOKEN;
  • the personnel ingestIngestRead supplied records or documents and add their information to NovusLens, after the required checks. connectorConnectorSoftware that reads records from a source system, such as an incident tool. A customer-written file connects those records to teams or services in NovusLens., which checks CONWAY_IDENTITY_SALT;
  • the installation checks, which inspect all four secrets without printing their values.

service_token() returns CONWAY_SERVICE_TOKEN, or an empty string when it is not set.

The profile changes behaviour in several places. Customer requests require x-conway-service-token, except on /health and /ready. A missing or wrong value returns 401. Its MCPMCPModel Context Protocol: a standard way for an AI application to discover and call named tools supplied by another program. tools are not registered on the MyceliumMyceliumThe application infrastructure underneath NovusLens. Its connected services exchange updates and keep the ordered structural log. gateway unless NOVUSLENS_MCP_EXPOSE is 1. The graphGraphA model made of units and links between them. In NovusLens it records teams, services and their dependencies. database connection refuses to fall back to the development DATABASE_URL and raises a configuration error instead.

Federation documents

common_py.federation signs and checks documents with Ed25519Ed25519The digital-signature method used to sign evidence. The private key signs the exact content; the public key checks that signature.. This is how a fact about a dependencyDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else. that crosses a region boundary travels between clustersClusterA group of computers running together as one installation. Each regional NovusLens installation keeps its own records.. Data stays within a region because each region runs a separate cluster. GossipGossipA way for connected services to pass updates to one another until the updates spread through the group. updates do not cross that boundary. The only thing that crosses is a self-certified signed document.

Each signed document is a dictionary with four keys: document, alg (always ed25519), public_key_b64 and signature_b64. The signatureDigital signatureA check calculated with a signing key that can detect whether a record has changed. Identifying the signer also requires a trusted link between that key and its owner. covers the canonical bytes of document. Canonical formCanonical JSONA fixed way to write data: sort the field names and omit spaces between fields. Do not use infinity or invalid numbers. Sender and receiver can then sign and check identical content. is JSONJSONJavaScript Object Notation: a text format for data, using named fields, lists and values. Configuration files and service messages use it. with sorted keys and separators , and :, encoded as UTF-8UTF-8A standard way to represent written characters as bytes, including characters outside the English alphabet..

Keys are derived, not stored. _region_key(region) takes the SHA-256SHA-256A method that turns content into a fixed-length fingerprint. Comparing fingerprints helps check whether content changed. of the text conway-region-identity: followed by the region name, and uses that 32-byte value as the Ed25519 seed. The same name always gives the same key, so a verifier can record that public keyPublic keyA value used to check a digital signature. Sharing it lets others check signatures but does not let them create new signatures. and compare later signatures with it. The derivation is public: anyone who knows a region name can recompute its private keyPrivate keyA secret value used to create digital signatures. The source keeps it so others cannot sign evidence in its name.. The module docstring says this plainly, and so does this page. What the check proves is that a document has not changed since it was signed. It does not prove who signed it. The module describes the production intent as the region’s real Mycelium identity key, which is only available from the Rust side.

The public functions are:

  • sign_fact(payload) signs a cross-region dependency under from_region. The payloadPayloadThe data carried inside a message, apart from the surrounding fields that identify and date it. must carry the eight FACT_FIELDS (group_id, from_region, from_org, from_cell, to_region, to_org, to_cell and capability), or it raises ValueError.
  • sign_document(document, signer) signs any document under any identity name. An operational agent uses it to publish its declared capabilityCapabilityWhat a team can provide or an agent can do. NovusLens records the claim and the evidence that supports it. and authorityAuthorityWhat a team or agent is allowed to decide or do. For an agent, this comes from a named person or system that grants permission..
  • sign_document_with(document, key) signs a document with a given Ed25519 private key. new_signing_key() makes a fresh key, and public_key_b64(key) returns the public key to enrol. The Meridian demo seed uses them, because the customer profile refuses the public fixtureFixtureA prepared set of records used for a test or demonstration. A fixture is sample data, not a live customer's records. keys.
  • verify_signature(signed) returns True only when alg is ed25519 and the signature verifies over the canonical bytes under the embedded key. It returns False for a bad signature, a missing key or malformed encoding.
  • verify_trusted(signed) adds the trust decision. It requires the embedded key to equal region_public_key_b64(document["from_region"]). A document signed with a different key fails here even when it is internally consistent.
  • fact_id(signed) is the SHA-256 hex digestHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records. of the canonical document. It is the content addressContent addressAn identifier calculated from a record's contents. It lets the program check that the retrieved bytes are the same bytes originally stored. used for de-duplication.
  • agent_facts_document(region, cluster=None, facts=None) builds the region’s own signed document. It publishes the region’s key and the facts it emits, with a certification block marked self-certified. The reasoning service serves it at /.well-known/agent-facts.json.

A signed document that is changed after signing has different canonical bytes, so its signature no longer verifies. A document signed by an unknown key passes verify_signature but fails verify_trusted.

CPL, the critical path length

CPL (critical path length) is the greatest total coordination weight along a dependency chain in the 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.. It is reported in index pointsIndex pointsThe unit of the dependency-chain index. Index points are structural weights, not hours or money, and every answer and what-if reports the index in them., not elapsed time. common_py.cpl.longest_path(edges, budget=DEFAULT_BUDGET) takes (consumer, provider, overhead_hours) triples and returns (cpl_hours, path). Empty input returns (0.0, []). The caller passes the worst-case weight for each consumer and provider pair when an edge is contested.

The module has two methods, and both give the same answer for an acyclic graph:

  • When the graph has no cyclesDependency cycleA loop in the dependencies: one unit needs another, which eventually needs the first unit again. For example, Sales waits on Finance and Finance waits on Sales., _dag_longest computes the longest suffix from each node by saving and reusing intermediate results. The work grows in proportion to the graph size. Ties are broken by the first neighbour in insertion order, which is the path the original depth-first search would have recorded.
  • When the graph has cycles, _dfs_longest runs the original exhaustive search for the longest simple path. It stops after DEFAULT_BUDGET, which is 2,000,000 node expansions, and raises CplBudgetExceeded. The exception carries the nodes that lie on cycles, found by cyclic_nodes by grouping nodes that can reach one another.

The search never changes the metric to finish sooner. If the budget runs out, conway_graph records a finding naming the cyclic 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.. The figure is reported as not computable within budget. The reasoning service’s conway_reasoning.tools uses longest_path for the base and scenario comparison. A scenario is indicativeIndicativeThe label on a scenario or what-if. An indicative result shows one possibility under stated assumptions. It is never a measurement of the future. only.

The supply-chain audit

common_py.audit_gate is the step that decides whether the production dependencies carry an untriaged advisoryAdvisoryA published report of a security problem in a software package.. The cmd_test function in ops runs the audit in this order:

  1. pnpm audit --prod --json runs in apps/ui and writes node-audit.json. The exit code is kept and passed on as --node-exit.
  2. uv export --frozen writes requirements-audit.txt. Lines that contain mycelium, comment lines and editable -e lines are removed before the file is passed on.
  3. pip-audit --format json runs on that file and writes python-audit.json and python-audit.err.

Then python -m common_py.audit_gate reads both reports against config/audit-triage.json. Each ecosystem gets one of three outcomes:

  • clean means a well-formed report with no advisory outside the triage file.
  • advisories means a well-formed report that names advisories outside the triage file. Each one is listed by name.
  • incomplete means the report is missing, empty, not JSON or not the expected shape. For Python it also covers a dependency that carries a skip_reason, and a report with no dependencies at all. For Node, a well-formed report needs both an advisories object and a metadata object. A non-zero pnpm exit with a well-formed report is a completed audit.

Only clean passes. The gate exits with 1 and prints a TEST FAIL line for each failing ecosystem. An advisory is accepted in Node by its GitHub advisory ID, and in Python by its ID or any alias.

The triage file holds an advisories list. Each entry needs an id, a status of fixed, mitigated, not applicable or not-applicable, and a non-empty reason. A malformed entry stops the gate before any report is read. The file records the braces advisory GHSA-vfj7-8cjw-p6xm as not applicable, with a reason. A completed audit passes when every reported advisory is accepted by this file.

Finding visibility

common_py.finding_scope decides who may see a finding. It is the single rule for every surface, so a finding that one screen hides cannot appear on another. The rule is written in config/finding-scope.json. The module docstring names the matching TypeScript copy, apps/ui/lib/findingScope.ts, and says that both are tested against the same cases.

classify(kind, detail) puts each finding into one class, checked in this order:

  1. estate: the kind is in estate_kinds or starts with boundary-. These compare teams and name them, so only the full view sees them.
  2. edge: the consumer and provider come from the edge keys for the kind, or from consumer and provider by default.
  3. cell: a single cell is named, through the cell key for the kind or cell by default.
  4. org-wide: the kind is in org_wide_kinds (currently empty), or the detail carries scope set to org-wide.
  5. unscoped: none of the above. Only the full view sees it, and needs_repair flags it for an operator.

Unknown 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. never widens visibility. visibility(kind, detail, visible) returns full, partial or hidden. A visible of None means the full view. For an edge, a viewer of both ends sees it whole, a viewer of one end sees partial, and a viewer of neither sees nothing. present(finding, visible) returns None for a hidden finding. For a partial finding, it names only the visible end and its capability. It removes other details and sets scope_partial. anchor_cells lists the cells a finding is about, for subscriptions and for each cell’s own page.

Agent evidence

common_py.agent_evidence is the Agent Evidence 1.0 core. It contains pure functions for checking and combining records. It does no input or output. Each record carries version 1, a kind, and the fields id, source, subject, at and received_at. The twelve KINDS cover grants, operations, intervention requests, authorisations, executions, outcomes, acceptances, corrections, withdrawals, remitsRemitA written list of an agent’s allowed actions, resources and destinations for information. The list states whether it is complete., activities and policy deployments.

validate(record) applies the bounds that the code sets:

  • text is non-empty and at most 512 characters;
  • timestamps are non-negative integer milliseconds;
  • numbers are integers or plain decimal strings, with a magnitude of at most 1e30. Supply decimals as text, for example "0.1";
  • lists hold at most 1,000 entries and contain no duplicates;
  • a canonical record is at most 64 KiBKiB and MiBBinary size units: one KiB is 1,024 bytes; one MiB is 1,048,576 bytes. Evidence limits use these units.;
  • a payload field that the kind does not declare is refused, so that no condition is silently ignored.

Each kind has its own rules as well. An authority grant’s interval must be non-empty. A remit must constrain at least one of operations, resources or destinations. An activity observation’s runtimeRuntimeThe software that runs a model or agent. A model runtime answers model requests; an agent runtime carries out the customer's agent actions. decision is permit, deny or indeterminate, and an execution is attempted, blocked, completed, failed or unknown. A mapping status is mapped, unmapped or ambiguous. The code does not infer a value that a record leaves out.

history(records, known_at) builds the state as it was known at a given time. Records received after known_at are ignored. The same ID with different content raises ValueError, and is never resolved by taking the later record. A correctionCorrectionA change to a recorded value or record. The earlier version stays in the history, and any conclusion that depended on it is checked again. or withdrawalWithdrawalA source’s removal of a value or record from current use. Its history stays, and NovusLens checks conclusions that depended on it again. takes effect only when its target is known, comes from the same source and subject, and is not itself an amendment. A withdrawal makes its target inactive at once. A correction whose replacement has not arrived also makes its target inactive, and is recorded as pending. Chains deeper than 64 raise an error, and so does a loop in the replacement links. The result carries records, active, inactive_ids, pending and a revision, which is the digest of every known record.

canonical(value) is the sorted-key JSON used everywhere, and digest(value) is its SHA-256 hex digest. validate_recovery_link requires exactly two text links, request_id and outcome_record_id.

Interfaces

ModuleMain namesUsed by (examples)
profileprofile, is_customer, secret_problems, require_customer_safe, service_token, CUSTOMER_SECRETSapps/reasoning/src/conway_reasoning/serve.py (start-up and service-token check), boundary_policy.py, car.py; apps/graph/src/conway_graph/db.py; apps/connectors/src/conway_connectors/personnel.py, boundary_acquire.py, agent_ingest.py; scripts/lens/preflight_service.py (installation checks)
federationsign_fact, sign_document, sign_document_with, new_signing_key, public_key_b64, verify_signature, verify_trusted, fact_id, agent_facts_document, region_public_key_b64, identity_public_key_b64apps/reasoning/src/conway_reasoning/serve.py, agent_evidence.py, boundary_policy.py; apps/graph/src/conway_graph/group.py; apps/mesh/src/conway_mesh/seed_walk.py; apps/connectors/src/conway_connectors/agent_producer.py, agent_ingest.py, boundary_contract.py, sim_estate.py; the federationFederationSharing selected signed facts and summaries between separate regional twins, while their detailed records stay in their own regions. step in ops
cpllongest_path, CplBudgetExceeded, cyclic_nodes, DEFAULT_BUDGETapps/graph/src/conway_graph/engine.py; apps/reasoning/src/conway_reasoning/tools.py
audit_gatemain, node_outcome, python_outcome, load_triagecmd_test in ops; packages/common_py/tests/test_audit_gate.py
finding_scopeclassify, visibility, present, anchor_cells, needs_repairapps/graph/src/conway_graph/__main__.py; apps/reasoning/src/conway_reasoning/ (finding_support.py, notify.py, questions.py, assurance.py, evidence_view.py, tools.py)
agent_evidencevalidate, history, canonical, digest, KINDS, validate_recovery_linkapps/graph/src/conway_graph/agent_evidence_store.py, agent_authority.py, remit.py, governance.py, fleet.py; apps/reasoning/src/conway_reasoning/agent_evidence.py, agent_policy.py, remit_authoring.py, car.py; apps/connectors/src/conway_connectors/agent_source.py, agent_producer.py, car_axes.py; apps/meaning/src/conway_meaning/deep_sweep.py

The list is representative. Tests import the same modules, and the connectors and graph use agent_evidence in more places than are shown here.

Invariants and failure behaviour

  • Profile. In the customer profile a missing or development secret stops the process at start-up, and the message names every problem. In development the same secrets fall back to public values, and nothing in profile.py itself emits a warning.
  • Federation. A document that is changed after signing fails verification. A document that is signed under the wrong key fails the trust check. Trust in a region is a choice the fetcher makes, and there is no shared issuing authority. The region key is derivable by anyone, so a passing check shows integrity and does not show authenticity.
  • CPL. The metric is the same whichever method runs. When the search budget is exceeded, the caller receives an exception that names the cyclic nodes. A cycle is never broken silently to get a number.
  • Audit. Only a completed, clean report passes. An untriaged advisory, a malformed triage entry or a missing report each fails the gate with a named reason.
  • Finding visibility. A finding that does not name its scope is hidden from scoped viewers and shown only to the full view. An estate finding is never shown to a scoped viewer, even when its detail names a visible cell.
  • Agent evidence. The code refuses records it cannot classify, conflicting reuse of an ID and corrections that do not match their target. Values the source omits remain unstated. No source-supplied field upgrades the trust placed in a record.
The order in which a finding is classified, and what each viewer sees for each class.
Last updated