audit¶
Append-only JSONL audit trail for routed tool calls: build_record,
append_record, artifact_paths and resolve_audit_path, plus the
redaction helpers that keep prompts, image payloads and credentials out of the
trail.
See also
Insiders header, routing and audit
for when a record is written, and
Configuration for MCP_AUDIT_LOG_PATH.
Append-only JSONL audit trail for routed tool calls.
One record per line, one os.write per record. The file is opened with
O_APPEND so concurrent writers append whole lines rather than overwrite one
another: a crash mid-write can lose a line but never corrupt a line already
written.
Nothing here may break a tool call. Every I/O or serialisation failure is logged and swallowed, because observability must never become a dependency of the business path.
Credentials and image payloads are never recorded. Prompts are reduced to their length plus a truncated SHA-256, image arguments are replaced by a marker, and any over-long string is digested instead of written verbatim, so a base64 blob cannot reach the trail even under an unexpected argument name.
- novelai_image_mcp.audit.AUDIT_FILENAME = 'audit.jsonl'¶
File name used when no explicit audit path is configured.
- novelai_image_mcp.audit.DIGEST_LENGTH = 16¶
Number of hex characters kept from a digest.
- novelai_image_mcp.audit.MAX_ARGUMENT_LENGTH = 256¶
Longest argument string recorded verbatim; longer values are digested.
- novelai_image_mcp.audit.MAX_RECORDED_HEADERS = 32¶
Largest number of headers kept in an audit record. The routing layer already caps its snapshot at 64; this is a second, tighter bound so a hostile client cannot grow an audit line without limit.
- novelai_image_mcp.audit.MAX_RECORDED_HEADER_LENGTH = 256¶
Longest header value kept in an audit record.
- novelai_image_mcp.audit.append_record(record, *, path)[source]¶
Append
recordas a single JSON line; return whether it was written.The record is serialised first and then handed to exactly one
os.writeon a descriptor openedO_APPEND, so concurrent writers cannot interleave halves of a line. Failures are logged and reported asFalse– this function never raises, because a tool call must not fail on account of the audit trail.
- novelai_image_mcp.audit.artifact_paths(result)[source]¶
Return the sorted
*.pngpaths a tool result mentions.Image tools report what they wrote as text (
Saved 1 image(s): [...]); this reads those paths back for the audit record. The result is only observed, never rewritten, and a result without text blocks yields[].
- novelai_image_mcp.audit.build_record(*, routing, tool, duration_ms, outcome, features=(), artifacts=(), arguments=None, unknown_keys=(), malformed=())[source]¶
Build one JSON-serialisable audit record.
features,artifacts,unknown_keysandmalformedare sorted so two records describing the same state are byte-identical.
- novelai_image_mcp.audit.cap_headers(headers)[source]¶
Apply the audit-side cap to an already-sanitised header snapshot.
The input is expected to be
RoutingContext.recorded_headers, which the routing layer has already stripped of every credential-bearing name. This only bounds the size: at most :data:MAX_RECORDED_HEADERSentries, each value at most :data:MAX_RECORDED_HEADER_LENGTHcharacters, kept in lexicographic order of the (lower-cased) name.
- novelai_image_mcp.audit.digest_text(value)[source]¶
Return the length and truncated SHA-256 of
value, never the text.
- novelai_image_mcp.audit.resolve_audit_path(*, audit_log_path, output_dir=None)[source]¶
Resolve the audit destination.
An explicit
audit_log_pathwins; otherwise the trail lives beside the generated images at<output_dir>/audit.jsonl.
- novelai_image_mcp.audit.summarise_arguments(arguments)[source]¶
Summarise tool arguments so the record can never leak a payload.
Prompts become a length plus a truncated SHA-256, image arguments become a marker, and every other value is reduced to something small and JSON-serialisable.
When a record is written¶
The audit trail is part of the insiders surface: ToolCallLoggingMiddleware
appends one record per tool call, and only while model-routing is
active. A request that does not send the opt-in header writes nothing.
resolve_audit_path picks the destination — an explicit
MCP_AUDIT_LOG_PATH wins, otherwise audit.jsonl inside
NOVELAI_OUTPUT_DIR.
Record fields¶
build_record emits one flat, JSON-serialisable object per call:
Field |
Content |
|---|---|
|
UTC timestamp, ISO-8601. |
|
The sanitised route. |
|
The extracted routing value, or |
|
Tool name. |
|
Wall-clock duration, rounded to three decimals. |
|
|
|
Sorted names of the active insiders features. |
|
Sorted |
|
Sorted unrecognised keys from the insiders header. |
|
Sorted unusable entries from the insiders header. |
|
Sanitised routing header snapshot, capped at |
|
Summarised call arguments — see below. |
features, artifacts, unknown_keys and malformed are sorted so two
records describing the same state are byte-identical.
Redaction¶
summarise_arguments reduces every argument before it can reach the file:
Prompt-shaped arguments (
prompt,prompts,negative_prompt,negativeprompt) become{"length": n, "sha256": "<first 16 hex characters>"}viadigest_text. The prompt text is never written.Image-shaped arguments (
image,images,init_image,mask,reference,references,base64) become the literal[redacted image].Any string longer than
MAX_ARGUMENT_LENGTH(256) characters is digested rather than written, so a base64 blob cannot slip through under an unexpected argument name.Mappings are reduced to
{"keys": n}; lists, tuples and sets to{"items": n}; any other object to its type name.None, booleans, ints and floats are recorded as they are.
Credential-bearing headers are removed upstream by
routing._recorded, so the snapshot this module caps never contains
authorization, cookie, set-cookie, proxy-authorization or any
*-token / *-secret / *-password / *-key / *-credential
header in the first place.
Concurrency and durability¶
append_record serialises the record first, then writes it with exactly
one os.write on a descriptor opened with O_APPEND (mode 0o600,
creating the parent directory when needed). Concurrent writers therefore append
whole lines instead of interleaving halves of one: a crash mid-write can lose a
line, but it cannot corrupt a line that was already written.
Failure behaviour¶
Observability never becomes a dependency of the business path.
append_record catches its own OSError / TypeError / ValueError, logs
the failure with logger.error, and returns False — it never raises, so
there is no code path in which an unwritable audit file turns into a failed
tool call.
See also¶
Insiders header, routing and audit — the header grammar and route resolution
middleware— the caller that appends the recordsrouting— the header snapshot this module caps