Status: A1 pure-schema implementation
Every MCP call must leave a durable, server-authored witness answering who attempted which operation, against which fork, when, and what the server knows about its outcome. The witness is an append-only journal outside the fork lifecycle. Ordinary MCP sessions cannot inspect or mutate it.
The journal records all six v1 verbs (assert, unassert,
unassert_handle, query, argue, and matching) and every denial. The
first three may mutate a fork; query, argue, and matching are reads.
This design follows an established durable-journal discipline and a separated audit plane: the audit record is its own append-only store, outside the fork's own lifecycle, with no network audit service or distributed transaction in v1.
Attempt, Completion, and optional Resolution.Attempt is durable before dispatch. A Completion is durable before a
normal response is returned.outcome_indeterminate.Resolution; it never rewrites history or manufactures a
missing Completion.Every stored record has:
| Field | Type | Meaning |
|---|---|---|
schema_version | integer | Exactly 1 |
record_type | enum | attempt, completion, or resolution |
record_id | UUID string | Unique journal-record identity |
operation_id | UUID string | Joins lifecycle records for one attempt |
sequence | decimal string | Monotonic journal sequence |
recorded_at | RFC 3339 UTC string | Server-authored event time |
A1's pure constructors do not invent record_id, sequence, or
recorded_at; the durable journal writer adds them at the durability
boundary.
An Attempt adds:
verb: one of the six verb names (matching was added after the original
five were frozen; the record shape is unchanged).auth_status: authenticated, missing, invalid, binding_missing, or
fork_unavailable.principal, session_id, and fork_id: server-resolved strings when known,
otherwise null.correlation_id: optional transport correlation identity. It is not an
idempotency key.input: exactly one tagged input variant below.input_digest: sha256:<lowercase hex> over the canonical input bytes.Authenticated input variants:
| Tag | Fields |
|---|---|
assert | sentence_edn, context_edn |
unassert | sentence_edn, context_edn |
unassert_handle | sentex_handle as a decimal string |
query | goal_edn, context_edn, include_justification boolean |
argue | closed_goal_edn, context_edn |
matching | pattern_edn, context_edn, asserted_only boolean, limit positive integer or null |
The *_edn fields contain normalized EDN values, not caller text.
Known limitation (matching): the dispatcher builds the matching Attempt
from its own read of the arguments, recording a malformed asserted_only as
false and an invalid limit as null, so the durable Attempt does not
witness the bad caller input the verb then rejects (the rejected Completion
still records the refusal).
An attempt whose principal authentication has not succeeded (missing or
invalid) uses the untrusted variant:
raw_sha256byte_lengthcontent_typeIt never contains the raw body. content_type, when present, is a string of at
most 256 characters. Pre-auth attempts must leave principal, session_id,
and fork_id null; an unauthenticated caller cannot assert server-resolved
identity into the audit record.
binding_missing and fork_unavailable happen after principal authentication.
They therefore retain the normalized typed input and known principal/session
identity while leaving the unresolved fork field null when necessary.
A Completion contains exactly one outcome variant:
succeeded: effect (changed, unchanged, or none) and a typed
result_summary.rejected: stable code and bounded message for schema/input rejection.denied: stable code and bounded message for authorization or binding
refusal.failed: stable code and bounded message only when the server knows no
effect happened.outcome_indeterminate: stage plus bounded observations when an effect
may have happened but cannot be proven either way.Messages are capped at 4096 UTF-8 bytes. Codes, not prose, are the stable contract. Every observations vector contains at most 32 nonblank strings; each string is capped at 1024 UTF-8 bytes and the vector at 8192 aggregate UTF-8 bytes. The same bound applies to Resolution observations.
Successful result summaries are:
| Verb | Fields |
|---|---|
assert | nullable handle, already_there boolean |
unassert | excepting_handles decimal-string vector, excepting_count |
unassert_handle | nullable excepting_handle decimal string |
query | answer_count, justifications_requested boolean |
argue | verdict, for_proof_present, against_proof_present |
matching | row_count, truncated boolean |
Full query answers, matching rows, bindings, and proof bodies do not enter the journal. Created exception handles do: they are needed both for useful receipts and for causal recovery evidence.
A Resolution contains exactly one variant:
effect_confirmed { evidence_ref }no_effect_confirmed { evidence_ref }indeterminate { observations[] }The two confirmed constructors accept only their outcome-specific evidence classes frozen by the recovery contract:
effect_confirmed accepts operation_recorded_handles { handles[] }: every
unique positive handle in the nonempty vector carries this operation_id.no_effect_confirmed accepts
pre_execution_denial { completion_record_id }, where the named durable
Completion proves dispatch never began, or
exclusive_no_write_receipt { evidence_id }, reserved for a future storage
revision whose exclusivity rules can causally prove no write occurred.A matching current postcondition, absence at inspection time, or a handle with
no recorded operation link is an observation, not causal proof. Those facts may construct only an
indeterminate Resolution.
Canonical records use UTF-8 JSON with lexicographically sorted object keys, no
insignificant whitespace, no floating-point numbers, decimal identifiers
encoded as strings, and lowercase sha256: digests. Arrays preserve semantic
order. EDN maps and sets must first be normalized into a deterministic tagged
representation; values that cannot be normalized are rejected before Attempt
construction.
Golden vectors (test-resources/audit_golden_vectors.json) are the
cross-language contract: any future non-Clojure adapter must produce
byte-identical canonical forms and digests for them.
The live path is:
receive -> resolve auth/binding -> durable Attempt -> dispatch -> durable Completion -> return
| Condition | Durable record / response |
|---|---|
| Attempt cannot persist | no work; native MCP audit_unavailable |
| Auth, binding, or validation refusal | Attempt + denied/rejected Completion |
| Successful read or mutation | Attempt + succeeded Completion, then normal response |
| Dispatch fails and no effect is known | Attempt + failed Completion |
| Dispatch began and effect is unknown | Attempt + indeterminate Completion; outcome_indeterminate |
| Completion fails after known no effect | bounded retry, then audit_unavailable |
| Completion fails after changed or possibly changed effect | bounded retry, then outcome_indeterminate |
| Process dies after Attempt | unmatched Attempt; recovery may append Resolution |
The initial Completion append gets two retries within a one-second total budget. These values are operational defaults and may be tuned without changing the outcome semantics.
Two invariants hold:
unassert currently creates exceptions sequentially. A later failure may
therefore follow earlier mutations. A future refactor should record the
operation id on every created exception handle, preserve known handles as observations, and
map any failure after dispatch begins to outcome_indeterminate unless
causal evidence proves no effect. It must never report that path as a
known-no-effect failed outcome.
The audit envelope observes calls admitted through the transport contract. Current direct-function quirks (extra keys, broad sequential values, truthy booleans, and inconsistent handle validation) are implementation gaps, not an alternative audit schema.
VAELII_AUDIT_DIR is an absolute, host-local directory outside fork storage,
temporary storage, and /tmp; directory mode is 0700, segment mode 0600.sync_data define the
commit point before projection or response.query plus assert, including denial and crash
tests.unassert, unassert_handle, and argue; exercise
partial-mutation semantics.A3 starts only after independent review finds A1 and A2 agree on exact bytes and semantics.
audit_id or correlation metadata if MCP cannot carry it
uniformly across native successes and errorsCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |