Changes outside the controlled path¶
From Genesis Mesh 1.3.0 the Network Authority (NA) records changes that did
not go through governedAction, and judges each one once, as of the time it
happened:
an observation: a change an observer saw at its source (a cloud activity log entry, or a reconciliation finding), signed by an observer key;
a break-glass record: a change a controller made while the NA could not be reached, with its caller’s justification, signed by the executor key.
The NA must run with EVIDENCE_STORE=on and EVIDENCE_OUT_OF_BAND=on. Until
then the routes answer 404 out_of_band_disabled, and records in the record
outbox wait. The operator’s side is described in
Changes Outside the Controlled Path.
Record outbox¶
Observations and break-glass records are kept in a record outbox until the NA
admits them, as FileOutbox keeps execution records. Give it a directory of
its own:
import { FileRecordOutbox, GenesisMeshClient } from 'genesis-mesh-sdk';
const gm = new GenesisMeshClient({
baseUrl, signingKeyBase64, keyId,
recordOutbox: new FileRecordOutbox('/var/lib/app/gm-records'),
});
await gm.evidenceStore.flushRecords(); // at startup and on a timer
enqueueRecord keeps a record and submits it; a transient failure leaves it
pending, and a refusal no retry can overcome (RECORD_PERMANENT_REFUSALS)
keeps it as a dead letter. A record the NA admits outside its time bounds is
kept by the NA as a quarantine entry and is listed in the run’s
quarantined. When the NA keeps a record it refuses for good as a quarantine
entry, the dead letter’s last_error.quarantine_id names it (1.7.0).
flushRecords submits pending break-glass records first, then (1.6.0) grant
uses, then observations up to 100 at a time, then (1.4.0) remediation
records, each in the order they were added. From 1.3.1:
a batch the NA refuses as a whole, such as one holding a record its JSON reader refuses (
invalid_json, a dead letter from 1.3.1), is split in halves until that record is sent alone, so one bad record never holds back the rest;after a batch the NA throttles (
429), batches are half the size, and nothing is sent until itsRetry-Afterhas passed, unlessignoreBackoff;a file of
FileRecordOutboxthat cannot be read is moved aside as<name>.unreadable: the first read throwsoutbox_file_unreadablenaming it, and the outbox works again from the next call.
One process uses a record outbox directory at a time. Nothing stops a second process from opening the same directory, and each would keep its own view of it: both submit the records they read, and one can undo the other’s updates. Give each process its own directory.
Observers¶
Register the observer’s key with role: 'observer', scoped to the resources
it watches (privileged operator key):
await gm.evidenceStore.registerExecutorKey({
key_id: 'activity-log-observer', public_key, executor_sovereign_id: 'cloud-observer',
role: 'observer', resource_prefix: 'kv:prod/',
});
Then sign each change with an ObservationRecorder:
import { ObservationRecorder, seedSigner } from 'genesis-mesh-sdk';
const observer = new ObservationRecorder({
observerSovereignId: 'cloud-observer',
signer: seedSigner(observerSeed, 'activity-log-observer'),
});
const observation = await observer.record({
resource_id: 'kv:prod/api-key', action: 'rotate', capability: 'secret.rotate',
changed_at: event.time, source: 'cloud-activity-log', source_event_id: event.id,
actor: event.principalHash, version_id: event.version,
metadata: { lifetime_days: 30 },
});
await gm.evidenceStore.enqueueRecord(observation);
changed_at, changed_not_before, changed_not_after and observed_at
take a Date or an ISO 8601 string with a UTC offset, such as the event’s
own time. From 1.3.1 the recorder signs them as the NA writes them, in UTC to
the microsecond (2026-10-10T08:00:00.573000Z); a finer fraction is cut, as
the NA reads it. A string without an offset, or that is not a time, is
refused before signing (observation_malformed).
Each source event is one observation. Unless you give observation_id, the
recorder derives it from the event (1.3.1): observationId(observer, source, source_event_id), the SHA-256 in hex of the three joined by NUL characters.
Recording an event again with the same fields signs the same record, and the
NA answers duplicate. A second record of the event with other fields, such
as a later observed_at, is refused as observation_conflict: when you read
an event again, take observed_at from what you recorded the first time, not
from the clock. If you give your own IDs, derive them from the event the same
way.
actor is recorded as the source reported it and is not authenticated: use
a pseudonymous identifier, never a credential. metadata, actor,
source_event_id and version_id pass the secret guard: names, versions and
times, never values. The guard is the NA’s own (1.3.1): together they are at
most 16 KiB of JSON with non-ASCII characters escaped, as the NA counts them
(an é counts 6 bytes, an emoji 12). The recorder refuses before signing
what the NA would refuse: a record its model does not take
(observation_malformed), secret material (observation_secret_material),
or (1.3.1) a value its JSON reader refuses, such as a lone surrogate or an
integer beyond 64 bits (StrictJsonError). Name the version
(version_id) when the source reports one: the NA matches the observation to
execution evidence for the same resource, action and capability that reports
the same execution_parameters.version_id, and the change is then governed
by that evidence’s decision, unless the observer’s own facts are denied by
the policies active then. A second observer’s report of the same version is
the same change.
A reconciliation finding knows only that a resource changed between two
scans. observationFromFinding turns one into an observation input with
that window; the NA judges it at both ends:
const findings = reconcileResources(inventory, await gm.evidenceStore.resourceStates());
for (const finding of findings) {
const input = observationFromFinding(finding, { capability: 'secret.rotate', previousScanAt, scannedAt });
if (input) await gm.evidenceStore.enqueueRecord(await observer.record(input));
}
Run again for the same scan, this signs the same records, which the NA
answers as duplicate. resourceStates counts break-glass records as the
resource’s changes, in the order the changes were made (1.3.1), so a
break-glass rotation is not found again as drift.
Break-glass¶
With breakGlass, governedAction runs the action even when the evaluation
fails transiently (network error, timeout, 5xx, 429), and keeps a signed
break-glass record in the record outbox. The action then gets a null
decision, and the result is a BreakGlassResult:
const result = await governedAction(gm, recorder, {
...params, breakGlass: { justification: 'incident 42: rotate the leaked key now' },
}, async decision => rotate(decision));
if ('brokeGlass' in result) {
console.warn(`ran without a decision (${result.failure}); recorded as ${result.record.break_glass_id}`);
}
A DENY never breaks the glass, nor does a decision that fails verification,
the NA throttling failed operator signatures (429 admin_auth_throttled),
an evaluation it could not store (503 evidence_store_unavailable), or any
other error. Before its first admin request a client reads the NA’s public
key from /sovereign.json (unless given audience); from 1.3.1, only a
lookup that gets no answer, or a 5xx or 429, breaks the glass there. Any
other answer, such as a firewall’s 403 or a 404, throws
na_public_key_unavailable with that status, and the action does not run.
breakGlass needs a record outbox, resource_id and an attestation-based
evaluation (attestation_id: an agreement-based one cannot be judged after
the fact). Before anything is evaluated or run it checks the record as it
will be signed, as the NA checks it: a justification of 1 to 1024 characters
(counted as the NA counts them, so an emoji is one), the context against the
secret guard, and room left for the action’s report within the record’s
16 KiB, with the justification counted. Every use shows in the resource’s
changes, with its justification; a policy can forbid it for a capability (a
denylist.v1 gate on parent_kind with the value break_glass).
Judgements and the state of a resource¶
The NA judges each record at admission unless that is turned off; the
operator judges the rest with judgeObservation and judgeBreakGlass.
resourceChanges lists the changes to a resource, with how it was governed
(prior_decision or after_the_fact) and its state (recorded, matched,
judged_allowed, judged_denied, indeterminate, observed,
quarantined). A judgement has no authorized field: it is never an
approval, and verifyEvidenceEvents refuses execution evidence that cites
one (evidence_cites_judgement). From 1.4.0 the NA pages the changes:
resourceChanges(resourceId, { limit, after_sequence }) reads one page
(limit 1 to 10000, default 1000; after_sequence the previous page’s
next_after_sequence), and allResourceChanges(resourceId) follows the
pages.
From 1.4.0, with the NA’s remediation records on, a denied, indeterminate or
flagged judgement arms a remediation or opens a review: the judgement carries
it (judgement.remediation), and the change’s state in resourceChanges
is its remediation state (remediation_due, overdue, remediated,
upheld, waived, or its Stage 2 state for an allowed change), with a
remediation object whose review_pending flag says a review is open. See
Remediation, reviews and urgent changes.
Verifying exports¶
verifyEvidenceEvents verifies the new entry kinds offline:
observations under observer keys and break-glass records under executor keys (
listExecutorKeysreturns each key’srole); an execution record signed by an observer key does not verify;judgements, quarantine and registry entries under the NA keys;
each resource’s observation positions (
observation_chain_break), one judgement per record (duplicate_judgement) that matches it (judgement_subject_mismatch,judgement_subject_missing), and execution evidence matched once (match_reused).
From 1.3.1 it checks, as the reference does:
that each record appears once per kind (
duplicate_record): a copied entry is a replay, reported and still checked like any record;that a Stage 2 entry’s envelope names what its record says (
envelope_mismatch): its ID, resource, action, capability and signer, a break-glass record’s attestation and outcome, a judgement’s subject, verdict and matched record, a quarantine’s refusal and a registry entry’s event;judgements by the kind and ID of the record they judge, so a break-glass record and an observation may share an ID;
that the record a judgement matched is in a run that starts at entry 1 (
judgement_match_missing);that a retention checkpoint removed as many entries as positions, after the checkpoint before it when the run holds that one (
retention_count_mismatch).
The result counts them in observations, break_glass, judgements and
quarantined. For the 1.4.0 remediation and review kinds, see
Remediation, reviews and urgent changes.