Agent evidence records, signed by the source that saw them.
Every fact about an agentAgentA program that takes actions for an organisation. NovusLens records what it is allowed to do and what its activity reports show.’s 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. and behaviour arrives as a signed batch from an enrolled sourceSourceA document, interview statement, measurement or export that supports a value. Each source has a date and a stated scope.. NovusLens does not declare any of these facts itself, apart from a remitRemitA written list of an agent’s allowed actions, resources and destinations for information. The list states whether it is complete. the organisation declares in the product. A record from an enrolled source is shown with its source named. A remit the product declared lands at the toldToldEvidence that somebody stated the value. It is the weakest level, and it is always attributed to its speaker. rungRungOne of three levels of support for a recorded value: told, shown or measured. The level appears beside the value.. Nothing on this page is inferred by NovusLens.
This page is what you hand a source team. For the full contract, including what each record means and the panel that reads them, see Agent evidence. For a worked export from a source, see Reference exporter.
What you will achieve
By the end of this page a source team will be able to:
- write a batch that the check command admits,
- sign it with the source’s own key,
- send it, and read the receiptReceiptA recorded acknowledgement of a request or action, with its identity, time and result. An evidence intake receipt confirms storage. It does not establish acceptance or prove that an agent acted., and
- correct or withdraw a record without deleting anything.
Docker commands
For the Compose installation, run commands from the checkout that holds .novuslens.env and compose.yml.
Prepare the input described below. Upload the batch into the running reasoning containerContainerA running program with its own packaged software and isolated environment, managed by Docker on the host., then check it with ./lens, which uploads the enrolmentEnrolmentThe operator's recorded permission for a source to send specified record types about named agents, using an identified signing key. itself:
./lens exec -- sh -c 'cat > /tmp/batch.json' < batch.json
./lens connectors agent-source check --org harbour --batch /tmp/batch.json --enrolment source.jsonUse your organisation id and the chosen account wherever a command asks for them. The producer belongs with the source. Keep its private keyPrivate keyA secret value used to create digital signatures. The source keeps it so others cannot sign evidence in its name. and durable checkpoint in storage controlled by the source team.
Upload a batch with ./lens exec as above and pass its /tmp/ path.
Follow the complete Operations manual for accounts, configuration and command access.
Before you start
You need an installation that is running, and an enrolment for the source. The enrolment is written by the operator. It names the source and its public keyPublic keyA value used to check a digital signature. Sharing it lets others check signatures but does not let them create new signatures.. It lists allowed record types, subjects and parties. The private key stays with the source. NovusLens holds only the public key.
Run the commands from the installation directory.
Check a batch before sending it
The check command reads the batch and, optionally, the enrolment. It writes nothing. It prints a line for each refused record, then a summary, and exits with status 1 when there is any problem.
./lens exec -- sh -c 'cat > /tmp/batch.json' < batch.json
./lens connectors agent-source check --org harbour --batch /tmp/batch.json --enrolment source.jsonA batch with no problems ends with the summary line and exits with status 0:
N record(s) checked · 0 problem(s) — admissibleWithout --enrolment the summary says — admissible; re-run with --enrolment for the rest, because the record types, subjects and parties can only be checked against an enrolment. A refused record prints as REFUSED <record id>: <problem>.
The enrolment document is written with agent-source enrol, and merged into the organisation with agent-source activate. Activation adds to what is already enrolled. It does not replace an earlier enrolment.
The batch
The source signs the canonical JSONCanonical 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. of document with its Ed25519Ed25519The digital-signature method used to sign evidence. The private key signs the exact content; the public key checks that signature. key. One batch holds one to a thousand records. A retry is the same batch id with identical content. Intake permits batches without a cursorCursorA saved position in a sequence of records. A program uses it to continue from where it stopped.. The reference producer requires one. When present, it must advance.
{
"document": {
"org": "harbour",
"source": "invoice-service",
"batch_id": "invoice-service-2026-09-12-001",
"cursor": {"stream": "approvals", "previous": null, "next": "2026-09-12T09:00:00Z#41"},
"records": []
},
"alg": "ed25519",
"public_key_b64": "<the source's public key, as enrolled>",
"signature_b64": "<signature over the canonical document>"
}| Field | Level | Required | What it is |
|---|---|---|---|
document | Top | Yes | The part that is signed. Its canonical JSON is what 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. |
org | Document | Yes | The organisation the batch is for. |
source | Document | Yes | The source’s stable id. Every record’s source must equal it. |
batch_id | Document | Yes | Unique to the batch. A retry reuses it. |
cursor | Document | No | stream, previous and next. previous is null for the first batch of a stream. next must differ from previous. |
records | Document | Yes | One to a thousand records. |
alg | Top | Yes | ed25519. |
public_key_b64 | Top | Yes | The source’s public key, as enrolled. |
signature_b64 | Top | Yes | The signature over the canonical document. |
The batch as a whole is limited to 2 MiBKiB and MiBBinary size units: one KiB is 1,024 bytes; one MiB is 1,048,576 bytes. Evidence limits use these units. in canonical form. The producer refuses a larger batch before it sends anything.
Every record
Every record carries the same envelopeEnvelopeThe surrounding fields that identify a message, its sender, its type and its time, alongside the message's data.. The server overwrites received_at when it stores the record.
| Field | Required | What it is |
|---|---|---|
version | Yes | 1. |
id | Yes | A stable identifier the source owns. It is unique per organisation, and once stored it is never changed. |
type | Yes | One of the record kinds below. |
source | Yes | The source’s stable id. It must equal the batch’s source. |
subject | Yes | The agent, as bound in the enrolment. |
at | Yes | The event time, in milliseconds. |
received_at | Yes | Set by the server on receipt. Any value a source sends is replaced. |
payload | Yes | The fields of the record kind. Unknown payloadPayloadThe data carried inside a message, apart from the surrounding fields that identify and date it. fields are refused. |
Record kinds
The validator accepts twelve kinds. The examples below show ten of them. The two not shown, acceptance_record and remit_declaration, are listed in the table with the fields the validator requires.
| Kind | Required payload fields | Permitted optional payload fields | Example below |
|---|---|---|---|
authority_grant | issuer, grant_version, operation, resource, unit | valid_from, valid_until, upper_bound | Yes |
operation | operation, resource, state | value, unit | Yes |
intervention_request | case_id, action, resource, requester, unit | value | Yes |
intervention_authorisation | request_id, authoriser, grant_id, decision | none | Yes |
intervention_execution | request_id, authorisation_id, executor, state, action, resource | none | Yes |
outcome_observation | execution_id, criterion, unit | window_start, window_end, complete, value | Yes |
acceptance_record | request_id, acceptor, decision, purpose, policy_ref, policy_version, resource | outcome_ids | No |
remit_declaration | issuer, remit_version, responsible_role | valid_from, valid_until, allowed, excluded, complete | No |
activity_observation | operation, resource, evidence_ref | destination, execution_identity, detector, classification, coverage, decision, execution, policy, mapping | Yes |
policy_deployment | policy_revision, deployed_by | activated_at, policy_digest, enforcement_points, coverage, implements, note | Yes |
correction | target_id, replacement_id, reason | none | Yes |
withdrawal | target_id, reason | none | Yes |
The examples below are illustrative. They show the shape of each kind, using invented identifiers.
Authority grant
What an agent may do, from the body that granted it. issuer must be enrolled for the source as a grant issuer.
{"version": 1, "id": "grant-procurement-3", "type": "authority_grant", "source": "finance-policy", "subject": "procurement-agent",
"at": 1757660400000, "received_at": 1757660400000,
"payload": {"issuer": "procurement-director", "grant_version": "3", "operation": "purchase-order.approve", "resource": "purchasing-account",
"unit": "GBP", "upper_bound": "10000", "valid_from": 1757660400000, "valid_until": 1789196400000}}Operation
A bounded action the protected system recorded, with its value. This is what a grant is compared with. State is attempted, completed, denied or failed. It comes from the ledger, never from the agent.
{"version": 1, "id": "po-4471-approve", "type": "operation", "source": "invoice-service", "subject": "procurement-agent",
"at": 1757664000000, "received_at": 1757664000000,
"payload": {"operation": "purchase-order.approve", "resource": "purchasing-account", "unit": "GBP", "value": "14250", "state": "completed"}}Intervention: request, authorisation, execution
Three records, from three sources, kept apart. A request from whoever asked. An authorisation from the person who may decide, naming the mandate. authoriser must be enrolled for the source. An execution from the runtimeRuntimeThe software that runs a model or agent. A model runtime answers model requests; an agent runtime carries out the customer's agent actions. that acted. A completed execution does not prove effect.
{"version": 1, "id": "req-4471", "type": "intervention_request", "source": "procurement-ops", "subject": "procurement-agent",
"at": 1757664600000, "received_at": 1757664600000,
"payload": {"case_id": "po-overrun-4471", "action": "pause-approvals", "resource": "purchasing-account", "requester": "procurement-ops", "unit": "intervention", "value": 1}}{"version": 1, "id": "auth-4471", "type": "intervention_authorisation", "source": "approval-service", "subject": "procurement-agent",
"at": 1757664900000, "received_at": 1757664900000,
"payload": {"request_id": "req-4471", "authoriser": "procurement-director", "grant_id": "mandate-pause-2026", "decision": "approved"}}{"version": 1, "id": "exec-4471", "type": "intervention_execution", "source": "agent-runtime", "subject": "procurement-agent",
"at": 1757665200000, "received_at": 1757665200000,
"payload": {"request_id": "req-4471", "authorisation_id": "auth-4471", "executor": "agent-runtime", "state": "completed", "action": "pause-approvals", "resource": "purchasing-account"}}Outcome observation
What an independent observer measured afterwards, for an exact criterion, over an explicit window, with a completeness claim. The criterion itself is recorded by the operator in the evidence policyEvidence policyThe operator’s rules for which agent reports count and for how long. The default period for grouping related events is fifteen minutes..
{"version": 1, "id": "outcome-4471", "type": "outcome_observation", "source": "ledger-monitor", "subject": "procurement-agent",
"at": 1757668800000, "received_at": 1757668800000,
"payload": {"execution_id": "exec-4471", "criterion": "no-approvals", "unit": "count", "window_start": 1757665200000, "window_end": 1757668800000, "complete": true, "value": 0}}Activity observation
One thing an agent was seen doing, compared with its remit. Operation, resource and destination are the three axes. CoverageCoverageWhat the available records cover, and what is missing. A count such as “3 of 5 dependencies measured” names the measured portion and the gap. is explicit. Optional fields describe the worker identity, detector classification, runtime decision and execution state. They can also record policy and catalogue revisions.
The execution state is one of attempted, blocked, completed, failed or unknown. blocked is the resource’s positive statement that it refused and did not execute.
{"version": 1, "id": "flow-9004", "type": "activity_observation", "source": "egress-collector", "subject": "procurement-agent",
"at": 1757670000000, "received_at": 1757670000000,
"payload": {"operation": "send-message", "resource": "supplier-portal", "destination": "203.0.113.9", "execution_identity": "session-7f2",
"evidence_ref": "flow-9004", "detector": {"name": "egress-monitor", "rule_version": "2.3"}, "classification": "unexpected egress",
"decision": "deny", "execution": "blocked", "policy": {"revision": "egress-v7", "enforcement_point": "egress-gateway"},
"mapping": {"catalogue": "procurement-actions", "revision": "2", "status": "mapped"},
"coverage": {"complete": true, "streams": ["process", "network"]}}}Deployment report
What a runtime says it activated: which policy revision, where, when, with what coverage, and which declarations it claims to enact. This is a report. It is not proof that every route is controlled.
{"version": 1, "id": "deploy-egress-v7", "type": "policy_deployment", "source": "agent-runtime", "subject": "procurement-agent",
"at": 1757659000000, "received_at": 1757659000000,
"payload": {"policy_revision": "egress-v7", "policy_digest": "sha256:3f1c…", "deployed_by": "platform-ci", "activated_at": 1757659000000,
"enforcement_points": ["egress-gateway"], "coverage": {"complete": false, "notes": "the batch export route is not gated"},
"implements": ["d-8a91f2c4e0b7-r1"]}}Correction and withdrawal
A correction is from the same source, about the same subject, and of the same type as the record it replaces. The original stays in the history. A withdrawalWithdrawalA source’s removal of a value or record from current use. Its history stays, and NovusLens checks conclusions that depended on it again. retracts support for a record. It deletes nothing.
{"version": 1, "id": "outcome-4471-correction", "type": "correction", "source": "ledger-monitor", "subject": "procurement-agent",
"at": 1757672400000, "received_at": 1757672400000,
"payload": {"target_id": "outcome-4471", "replacement_id": "outcome-4471-b", "reason": "the collector omitted one approval in the window"}}{"version": 1, "id": "grant-procurement-3-withdrawn", "type": "withdrawal", "source": "finance-policy", "subject": "procurement-agent",
"at": 1757676000000, "received_at": 1757676000000,
"payload": {"target_id": "grant-procurement-3", "reason": "mandate rescinded by the procurement director"}}What the enrolment must allow. Each source may publish only enrolled record types about subjects bound to 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.. It may name only the issuers, authorisers and acceptors listed by the operator. Intake checks record types, subjects, grant issuers, authorisers and acceptors before storing records. Remit issuers are checked when records are read. The pre-send checker also checks remit issuer enrolment. It does not check signatures, event clocks or current cell existence.
The sequence
The steps run in this order. Each one can be checked before the next starts.
- Build and sign. The source builds the batch and signs the canonical JSON of
documentwith its own Ed25519 key. - Check.
agent-source checkreads the batch and the enrolment and writes nothing. Fix every refused record and sign again. - Send. The source’s producer,
agent_producer.py, writes a durable checkpoint with the signed envelope before it sends anything. It reads the receipt first, then posts to the reasoning service. The request includes the viewer, organisation and signed batch. The service tokenService tokenA secret value sent with requests between NovusLens services. The receiving service refuses requests with a missing or wrong value., when one is used, is read from theCONWAY_SERVICE_TOKENenvironment variableEnvironment variableA named setting made available to a running program. The customer launcher reads these settings from the installation environment file. and sent asX-Conway-Service-Token. Plain HTTPHTTPHypertext Transfer Protocol: the rules a browser or program uses to send a request to a web service and receive its response. is accepted only forlocalhost,127.0.0.1and::1. - Store. The service stores the batch before it acknowledges it. The server sets
received_aton each record. The receipt readsstored-pending-projection. - Publish. The batch reference is published to the meshMeshA set of connected Mycelium services that exchange updates and keep the NovusLens log. log. The receipt reads
published-pending-projection. - Project. The batch is applied to the evidence record table, and each record is keyed by its id. The receipt reads
projected.
The producer checks each receipt against what it sent. The receipt must match the batch id, the source, the document hashHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records. and the cursor. A cursor whose previous does not match the checkpoint is refused before sending.
The guide states that the mapMapThe view of teams, services and their dependencies for the selected part of the organisation. Details appear beside the map. shows the result after the projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. service’s next pass, about 30 seconds by default. Historical reads include only evidence received by the requested time. Later receipts do not appear in an earlier view.
Resend, correction and withdrawal
| Situation | What happens |
|---|---|
| Stable identity | The record id is unique per organisation, owned by the source. The batch id is unique per organisation. Both are immutable once stored. |
| The same batch again | Acknowledged as a duplicate. Nothing new is stored. |
| The same batch id with different content | Refused with batch ID reused with different content. |
| The same record id with different content | Refused with immutable agent record ID conflict at projection. A read of that record refuses the same way. |
| A changed value | A correction naming the target and its replacement, from the same source, about the same subject and of the same type. The target becomes inactive and the replacement becomes active. |
| A withdrawal | The target becomes inactive. Nothing is deleted. There is no un-withdrawal: send a fresh record with a new id. |
| A correction whose replacement has not arrived | The target is inactive now, and the correction is listed as pending. |
| A correction of a correction | Pending. Only an original record can be corrected. |
| A record left out of a later batch | Nothing happens. Batches are additive, and an absence means nothing. |
| The cursor | previous must match the last acknowledged position. A reused position is refused with Producer cursor position must not be reused. A gap is refused with Producer cursor conflict. |
When something is refused
A refusal names the record or the batch and the reason. Nothing is stored for a refused batch.
| Refusal | Where it comes from | What to change |
|---|---|---|
| A batch with no records, or more than a thousand | agent_evidence_store.enqueue | Split the batch. |
| A batch larger than 2 MiB in canonical form | agent_evidence_store.enqueue and agent_producer | Split the batch. |
A record whose source differs from the batch’s source | agent_evidence_store.enqueue, and the check | Send each source’s records in its own batch. |
| A payload field the record kind does not permit | common_py.agent_evidence.validate | Remove the field. Unknown fields are never ignored silently. |
| A missing required field | common_py.agent_evidence.validate | Add the field the table above lists. |
| A record kind the enrolment does not list | agent_source.check | Ask the operator to enrol the kind. |
| A subject not bound to a cell in the enrolment | agent_source.check | Ask the operator to bind the subject. |
| A grant issuer, authoriser, acceptor or remit issuer not listed in the enrolment | agent_source.check | Ask the operator to add the party. |
| A signature that does not verify | agent_producer.send | Re-sign the canonical document with the enrolled key. |
| A receipt that does not match the batch | agent_producer.send | Query the receipt and do not resend until the mismatch is understood. |
A cursor whose previous does not match the checkpoint | agent_producer.send and agent_cursor.advance | Read the durable checkpoint, then resend from it. |
Cursor must advance to a distinct position | agent_cursor.validate | Send a next value that differs from previous. |
| A plain HTTP address that is not localhost | agent_producer.send | Use an HTTPSHTTPSHTTP carried over an encrypted connection, so the contents are protected while travelling between the caller and the server. address. |
artifact integrity failure or invalid agent evidence batch at projection | agent_evidence_store.apply_batch | The stored batch does not match its hash, or is malformed. Raise it with the operator. |
The check command exits with status 1 when it refuses any record.