<?xml version="1.0" encoding="utf-8"?>
<!-- xml2rfc v3 source per RFC 7991. Generate text or html using xml2rfc. -->
<rfc xmlns:xi="http://www.w3.org/2001/XInclude"
     ipr="trust200902"
     docName="draft-saihm-memory-protocol-03"
     category="info"
     submissionType="independent"
     xml:lang="en"
     tocInclude="true"
     tocDepth="4"
     sortRefs="true"
     version="3">
  <front>
    <title abbrev="SAIHM Protocol">The Sovereign AI Horizontal Memory (SAIHM) Protocol</title>
    <seriesInfo name="Internet-Draft" value="draft-saihm-memory-protocol-03"/>
    <author fullname="Russell Jackson" initials="R." surname="Jackson">
      <organization>SAIHM (Sovereign AI Horizontal Memory)</organization>
      <address>
        <email>architect@saihm.net</email>
        <uri>https://saihm.net/</uri>
      </address>
    </author>
    <date year="2026" month="October" day="11"/>
    <area>art</area>
    <workgroup>Independent Submission</workgroup>
    <keyword>AI memory</keyword>
    <keyword>post-quantum signatures</keyword>
    <keyword>Model Context Protocol</keyword>
    <keyword>GDPR Article 17</keyword>
    <keyword>erasure</keyword>
    <abstract>
      <t>This document defines the Sovereign AI Horizontal Memory (SAIHM)
         protocol, a memory layer for AI agents. A SAIHM operator stores
         an agent's memory as encrypted cells bound to a post-quantum
         holder identity, records each state change in a public audit
         log, and supports revocable sharing contracts. The protocol defines an
         erasure operation with two stated erasure classes, intended to
         help an operator meet its obligations under Article 17 of EU
         Regulation 2016/679 (GDPR).</t>
      <t>This revision specifies the custodial profile. The operator
         holds the holder's keys and performs all cryptography. The
         agent-side client holds no keys, runs no cryptography beyond
         TLS and keeps
         no store; it reaches the operator over HTTPS and exposes the
         protocol to the agent as eight Model Context Protocol (MCP)
         tools. The document defines the operator obligations, the
         HTTPS binding, the eight tools, and the checks a conformant
         client applies to what the operator returns.</t>
    </abstract>
  </front>
  <middle>
    <section anchor="introduction"><name>Introduction</name>
      <t>This document is published through the Independent Submission
         Stream of the RFC Series, as defined in <xref target="RFC4846"/>
         and described in <xref target="RFC8730"/>. It is not an Internet
         Standard, and it has not been reviewed for, nor does it
         necessarily reflect, the rough consensus of the IETF
         community.</t>
      <section anchor="motivation"><name>Motivation</name>
        <t>The Model Context Protocol <xref target="MCP"/>, donated to the
           Agentic AI Foundation <xref target="AAIF"/> under the Linux
           Foundation on 9 December 2025, defines how an AI agent reaches
           external tools and contextual data sources. It does not
           standardize how an agent persists memory across sessions,
           models, or vendors. Here, "memory" means the holder's
           content-addressed cells (<xref target="cell-shape"/>;
           <xref target="terminology"/>).</t>
        <t>Production agents today keep memory in a local file system,
           a vendor-specific session log, or a vector database. None of
           these gives a signed record of each memory and of each
           change to it, a public log of those changes, or an erasure
           operation with stated properties
           (<xref target="GDPR"/>, Article 17).</t>
        <t>SAIHM is a memory-layer protocol that an MCP-capable agent
           attaches to obtain durable, shareable and erasable memory.
           Nothing in it depends on a particular operator or
           vendor.</t>
      </section>
      <section anchor="custody-profiles"><name>The custodial profile</name>
        <t>This document specifies the custodial profile, in which the
           operator
           generates and holds the holder's identity key and every
           data-encryption key, encrypts on write, decrypts on read,
           signs cells and receipts on the holder's behalf, and returns
           plaintext to the authenticated holder.</t>
        <t>The client in this profile forwards
           each tool call to the operator over HTTPS, holds no key
           material, and validates the shape of each
           response before reporting it to the agent
           (<xref target="client-conformance"/>). Its security rests on
           the transport and on the operator, and
           <xref target="security"/> states what that does and does not
           protect.</t>
        <t>A profile in which the holder keeps the keys and the operator
           sees only ciphertext is possible under the same cell shape.
           It is outside the scope of this revision.</t>
      </section>
      <section anchor="scope"><name>Scope</name>
        <t>This document defines:</t>
        <ul>
          <li>The cell shape: an encrypted memory unit with canonical
              metadata.</li>
          <li>Holder identity bound to an ML-DSA-65 key pair
              (<xref target="FIPS204"/>) derived through HKDF
              (<xref target="RFC5869"/>).</li>
          <li>The encryption envelope, with a random per-cell
              data-encryption key wrapped under a versioned key-encryption
              key.</li>
          <li>Receipts recorded in a public audit log.</li>
          <li>Sharing contracts in three modes: temporary, permanent and
              syndicate.</li>
          <li>Two erasure classes, cryptographic and access-control, and
              the obligation on an operator to state which it
              provides.</li>
          <li>An HTTPS binding between the client and the operator.</li>
          <li>An MCP binding of eight tools, and the checks a client
              applies to each tool's result.</li>
        </ul>
        <t>This document does NOT define:</t>
        <ul>
          <li>How the operator stores ciphertext, or which audit log it
              uses.</li>
          <li>How the operator authenticates the holder. The client
              carries an operator-defined credential and treats it as
              opaque.</li>
          <li>Vector-database or retrieval-ranking semantics.</li>
          <li>Agent runtime semantics, which MCP and its host runtimes
              cover.</li>
        </ul>
      </section>
      <section anchor="terminology"><name>Conventions and Terminology</name>
        <t>The key words "<bcp14>MUST</bcp14>", "<bcp14>MUST NOT</bcp14>",
           "<bcp14>REQUIRED</bcp14>", "<bcp14>SHALL</bcp14>",
           "<bcp14>SHALL NOT</bcp14>", "<bcp14>SHOULD</bcp14>",
           "<bcp14>SHOULD NOT</bcp14>", "<bcp14>RECOMMENDED</bcp14>",
           "<bcp14>NOT RECOMMENDED</bcp14>", "<bcp14>MAY</bcp14>", and
           "<bcp14>OPTIONAL</bcp14>" in this document are to be
           interpreted as described in BCP 14
           <xref target="RFC2119"/> <xref target="RFC8174"/> when, and
           only when, they appear in all capitals, as shown here.</t>
        <dl>
          <dt>AEAD:</dt>
          <dd>Authenticated Encryption with Associated Data
              (<xref target="RFC5116"/>). AES-256-GCM
              (<xref target="NIST-SP-800-38D"/>) is
              <bcp14>RECOMMENDED</bcp14>.</dd>
          <dt>Cell:</dt>
          <dd>an encrypted memory unit (<xref target="cell-shape"/>).</dd>
          <dt>Client:</dt>
          <dd>the software on the agent's side that exposes the eight
              SAIHM tools to an MCP host and forwards each call to the
              operator endpoint. In the custodial profile the client
              holds no key material.</dd>
          <dt>Custodial operator:</dt>
          <dd>an operator that holds the holder's identity key and the
              data-encryption keys and performs all cryptographic
              operations on the holder's behalf.</dd>
          <dt>DEK:</dt>
          <dd>Data Encryption Key. A 32-byte symmetric key, unique to one
              cell, used with the AEAD cipher.</dd>
          <dt>Endpoint:</dt>
          <dd>the HTTPS URL at which an operator accepts tool calls
              (<xref target="operator-binding"/>).</dd>
          <dt>HKDF:</dt>
          <dd>HMAC-based Extract-and-Expand Key Derivation Function
              (<xref target="RFC5869"/>), instantiated with SHA-256.</dd>
          <dt>holder seed:</dt>
          <dd>a secret of at least 256 bits from which a holder's
              identity key is derived
              (<xref target="identity-binding"/>). In the custodial
              profile the operator generates and holds it.</dd>
          <dt>Holder:</dt>
          <dd>the natural person, organization, or autonomous agent on
              whose behalf cells are written. Each holder has one
              holderId.</dd>
          <dt>KEK:</dt>
          <dd>Key Encryption Key. An operator-held key that wraps DEKs.
              Each KEK generation has a 32-bit version number,
              kekVersion.</dd>
          <dt>MCP:</dt>
          <dd>the Model Context Protocol <xref target="MCP"/>. In MCP, a
              "tool" is a named operation with typed inputs and typed
              outputs that an MCP server exposes to a model.</dd>
          <dt>memory:</dt>
          <dd>the set of SAIHM cells bound to one holderId, less those
              that have been erased. This is distinct from memory in the
              computer-architecture sense.</dd>
          <dt>ML-DSA:</dt>
          <dd>Module-Lattice-Based Digital Signature Algorithm
              (<xref target="FIPS204"/>). SAIHM uses the ML-DSA-65
              parameter set.</dd>
          <dt>Operator:</dt>
          <dd>the party that runs an endpoint and provides storage,
              cryptography, audit logging and access control to
              holders.</dd>
          <dt>Receipt:</dt>
          <dd>a signed record of one operation, written to the public
              audit log (<xref target="audit-log"/>).</dd>
          <dt>Result identifier:</dt>
          <dd>the identifier an operator returns to name the object a
              write produced or acted on: a cellId, contractId or
              proposalId (<xref target="receipt-ids"/>). It is distinct
              from receiptId, which names an audit-log entry.</dd>
          <dt>sentinel value:</dt>
          <dd>an in-band value that signals the absence of an ordinary
              value of the same type. In
              <xref target="sharing-contracts"/>, an expiry of 0 means
              "no time bound".</dd>
          <dt>Shard:</dt>
          <dd>a cell as stored under a storage tier. A shard is named by
              its cellId, and sharing contracts list the cellIds they
              cover.</dd>
          <dt>storage tier:</dt>
          <dd>a named, operator-chosen persistence class with stated
              durability and retention properties. Tier names carry no
              protocol semantics.</dd>
          <dt>UNIX epoch:</dt>
          <dd>the time reference defined in
              <xref target="IEEE-1003.1-2024"/> under "Seconds Since the
              Epoch". SAIHM timestamps are unsigned integer seconds since
              the UNIX epoch.</dd>
        </dl>
      </section>
      <section anchor="substrate-prerequisites"><name>Substrate prerequisites</name>
        <t>A conformant operator depends on the following. Where a
           substrate is selected per deployment, the operator states its
           selection in its published documentation.</t>
        <ol type="(%c)">
          <li>A source of random bits suitable for key generation
              (<xref target="RFC4086"/>).</li>
          <li>HKDF with SHA-256 (<xref target="RFC5869"/>,
              <xref target="FIPS180-4"/>).</li>
          <li>ML-DSA-65 conformant with <xref target="FIPS204"/>.</li>
          <li>An AEAD cipher conformant with <xref target="RFC5116"/>;
              AES-256-GCM is <bcp14>RECOMMENDED</bcp14>.</li>
          <li>SHA-256 (<xref target="FIPS180-4"/>) for cellId
              derivation.</li>
          <li>A CBOR codec (<xref target="RFC8949"/>) for cells and
              receipts at rest.</li>
          <li>An HTTPS server (<xref target="RFC9110"/> over
              <xref target="RFC9846"/>) for the endpoint.</li>
          <li>A public, append-only audit log in which any party can
              fetch an entry by its identifier and list entries by
              subjectId, by holderId and by operation
              (<xref target="audit-log"/>). Selected per
              deployment.</li>
          <li>A storage tier offering put, get and delete over opaque
              ciphertext. Selected per deployment.</li>
        </ol>
        <t>A conformant client depends on an MCP server runtime, an HTTPS
           client, and a JSON parser (<xref target="RFC8259"/>). It needs
           no cryptographic library beyond the one its TLS stack
           uses.</t>
      </section>
    </section>

    <section anchor="architecture"><name>Architecture</name>
      <section anchor="cell-shape"><name>Cell shape</name>
        <t>A SAIHM cell is the tuple:</t>
        <artwork align="left">
&lt;cellId, holderId, kekVersion, tier, cellNonce, ciphertext,
 signature, timestamp&gt;
        </artwork>
        <dl>
          <dt>cellId:</dt>
          <dd>
            <t>32-byte content-derived identifier, computed as</t>
            <artwork align="left">
cellId = SHA-256( kekVersion_be32 ||
                  cellNonce       ||
                  ciphertext )
            </artwork>
            <t>where kekVersion_be32 is kekVersion as a 4-byte
               big-endian unsigned integer, ciphertext is the full AEAD
               output including the authentication tag, and || is
               octet concatenation. On the wire cellId is a lowercase
               hexadecimal string of 64 characters.</t>
            <t>Because kekVersion and cellNonce are inputs, the same
               plaintext written twice yields two distinct cellIds.</t>
            <t>When the operator reads a cell it <bcp14>MUST</bcp14>
               recompute cellId from the stored kekVersion, cellNonce
               and ciphertext, and <bcp14>MUST</bcp14> refuse to serve a
               cell whose stored cellId does not match.</t>
          </dd>
          <dt>holderId:</dt>
          <dd>32-byte holder identifier: the SHA-256 digest of the
              holder's ML-DSA-65 public key
              (<xref target="identity-binding"/>). On the wire it is a
              64-character lowercase hexadecimal string.</dd>
          <dt>kekVersion:</dt>
          <dd>32-bit unsigned integer naming the KEK generation in force
              when the cell was written. It is fixed for the life of the
              cell; later re-wrapping of the DEK does not change it
              (<xref target="encryption-envelope"/>).</dd>
          <dt>tier:</dt>
          <dd>short ASCII string naming the storage tier.</dd>
          <dt>cellNonce:</dt>
          <dd>16 random bytes, unique per cell. cellNonce is public and
              travels with the cell.</dd>
          <dt>ciphertext:</dt>
          <dd>the cell payload encrypted under the cell's DEK.</dd>
          <dt>signature:</dt>
          <dd>ML-DSA-65 signature made with the holder's identity key
              (<xref target="signed-messages"/>).</dd>
          <dt>timestamp:</dt>
          <dd>64-bit unsigned integer, UTC seconds since the UNIX epoch.
              Implementations <bcp14>MUST NOT</bcp14> use a 32-bit
              signed representation, which overflows at 03:14:07Z on
              19 January 2038.</dd>
        </dl>
      </section>

      <section anchor="identity-binding"><name>Identity binding (ML-DSA-65)</name>
        <t>A holder's identity key is derived from the holder seed:</t>
        <artwork align="left">
identityKey = HKDF-SHA-256(salt = "MPS-PQC-KEY-GEN-v1",
                           IKM  = holderSeed,
                           info = "MPS-AGENT-IDENTITY-v1",
                           L    = 64 bytes)
        </artwork>
        <t>The first 32 bytes of identityKey are the seed (xi) for
           ML-DSA-65 key generation (<xref target="FIPS204"/>,
           ML-DSA.KeyGen_internal). The remaining 32 bytes are reserved.
           holderId is SHA-256 of the resulting public key.</t>
        <t>In the custodial profile the operator generates the holder
           seed, derives the key pair, and holds the private key. The
           private key and the holder seed <bcp14>MUST NOT</bcp14> be
           sent to the client, written to a receipt, or returned by any
           tool. The operator signs each cell and each receipt with the
           holder's private key after it has authenticated the caller as
           that holder (<xref target="binding-auth"/>).</t>
        <t>When it creates a holder, the operator <bcp14>MUST</bcp14>
           write the holder's public key to the audit log in a KEY
           receipt (<xref target="audit-log"/>), which anyone can find
           by holderId. The holder cannot be used until that receipt is
           written. With that entry, anyone
           can check every receipt signature for the holder without the
           operator's help, and anyone given a cell can check the cell
           signature.</t>
      </section>

      <section anchor="signed-messages"><name>Signed messages</name>
        <t>Every signature in this protocol is ML-DSA-65 in its pure
           form (ML-DSA.Sign, <xref target="FIPS204"/>) with a context
           string that names what is signed, so that a signature over
           one kind of message cannot be presented as another:</t>
        <dl>
          <dt>Cell, context "SAIHM-CELL-v1":</dt>
          <dd>cellId || holderId || kekVersion_be32 || timestamp_be64,
              where timestamp_be64 is timestamp as 8 bytes,
              big-endian.</dd>
          <dt>Receipt, context "SAIHM-RECEIPT-v1":</dt>
          <dd>the deterministic CBOR encoding
              (<xref target="RFC8949"/>, Section 4.2.1) of the receipt
              map (<xref target="wire-formats"/>) without keys 1
              (receiptId) and 5 (signature). receiptId is SHA-256 of the
              same bytes.</dd>
          <dt>Contract, context "SAIHM-CONTRACT-v1":</dt>
          <dd>the deterministic CBOR encoding of the contract map
              (<xref target="wire-formats"/>) without keys 1
              (contractId) and 9 (signature), and including key 10
              (contractNonce). contractId is SHA-256 of
              the same bytes.</dd>
        </dl>
        <t>An operator <bcp14>MAY</bcp14> use the hedged (randomized)
           or the deterministic signing variant.</t>
      </section>

      <section anchor="encryption-envelope"><name>Encryption envelope</name>
        <t>For each new cell the operator:</t>
        <ol>
          <li>draws a fresh 32-byte DEK and a fresh 16-byte cellNonce
              from its random source;</li>
          <li>encrypts the payload under the DEK with the AEAD
              cipher;</li>
          <li>wraps the DEK under the current KEK generation and stores
              only the wrapped form, keyed by cellId;</li>
          <li>discards the unwrapped DEK once the write completes.</li>
        </ol>
        <t>The DEK <bcp14>MUST</bcp14> be independent of every other key
           the operator holds. In particular it <bcp14>MUST NOT</bcp14>
           be derivable from identityKey, from the holder seed, or from
           any other long-lived secret. Cryptographic erasure
           (<xref target="erasure"/>) depends on this
           (<xref target="security-erasure"/>).</t>
        <t>The AEAD nonce (for AES-256-GCM, the 96-bit IV) is chosen by
           the operator and <bcp14>MUST NOT</bcp14> repeat under one
           DEK. Because each DEK encrypts one payload, using the first
           12 bytes of cellNonce is sufficient, and the example in
           <xref target="appendix-crypto-values"/> does so. The cell
           does not name its AEAD algorithm or the key-wrap algorithm;
           both are operator-internal and stated in the operator's
           documentation.</t>
        <t>No associated data is used. The ciphertext is bound to the
           cell by cellId and by the cell signature, and the operator
           <bcp14>MUST</bcp14> check both before it decrypts
           (<xref target="tool-recall"/>).</t>
        <t>KEK rotation is versioned. An operator <bcp14>MAY</bcp14>
           rotate the KEK under its own policy and
           <bcp14>MAY</bcp14> re-wrap existing DEKs under the new
           generation. The wrapped DEK is stored with the version of the
           KEK that wrapped it, apart from the cell, so re-wrapping
           changes neither cellId nor the cell signature. An operator
           <bcp14>MUST</bcp14> re-wrap every DEK held under a KEK
           generation before it retires that generation.</t>
      </section>

      <section anchor="audit-log"><name>Audit log</name>
        <t>For each operation that needs one (<xref target="receipt-semantics"/>, and the KEY receipt of
           <xref target="identity-binding"/>) the operator writes a
           receipt:</t>
        <artwork align="left">
&lt;receiptId, subjectId, operation, holderId, signature, timestamp,
 receiptNonce [, detail]&gt;
        </artwork>
        <t>subjectId is the cellId, contractId or proposalId the
           operation acted on, or the holderId for KEY, RECALL and STATUS
           receipts.
           operation is one of "KEY", "REMEMBER", "RECALL", "STATUS",
           "FORGET", "SHARE", "REVOKE", "PROPOSE" or "VOTE". A KEY
           receipt also carries the holder's ML-DSA-65 public key, a
           PROPOSE receipt carries the proposal, and a VOTE receipt
           carries the vote's direction and weight. Every
           receipt carries 16 random bytes, receiptNonce, so that two
           otherwise identical receipts have different receiptIds.</t>
        <t>The receipt is written to a public, append-only audit log.
           The log <bcp14>MUST</bcp14> let any party fetch an entry by
           its receiptId and list entries by subjectId, by holderId and
           by operation (for example all VOTE receipts for a proposalId,
           a holder's KEY receipt, or every FORGET receipt), so that a
           third party holding a receiptId can check the
           receipt's signature against the holder's public key. An
           operator <bcp14>SHOULD</bcp14> return the receiptId in the
           result of every state-changing tool, as the member
           "receiptId"; saihm_forget returns it as destructionAnchor.
           The choice of log is a deployment decision.</t>
      </section>

      <section anchor="sharing-contracts"><name>Sharing contracts</name>
        <t>A holder <bcp14>MAY</bcp14> grant other agents access to some
           of its shards through a sharing contract:</t>
        <artwork align="left">
&lt;contractId, holderId, granteeIds, shardIds, mode, scope,
 expiresAt, timestamp, signature, contractNonce&gt;
        </artwork>
        <t>granteeIds is a non-empty list of 32-byte holderIds with no
           duplicates; the operator <bcp14>MUST</bcp14> reject any entry
           of another length. shardIds is a non-empty list of the
           holder's cellIds that the contract covers; nothing outside it
           is exposed, and the contract does not copy the cells.</t>
        <t>scope is "read", "write" or "readwrite". This revision
           defines only "read", which lets a grantee receive the covered
           cells from saihm_recall. None of the eight tools lets a
           grantee write into another holder's memory, so an operator
           <bcp14>MUST</bcp14> reject "write" and "readwrite" with the
           error "invalid_scope". They are reserved for a later
           revision.</t>
        <t>mode is one of:</t>
        <dl>
          <dt>TEMPORARY:</dt>
          <dd>revocable; expiresAt <bcp14>MUST</bcp14> be later than the
              contract timestamp and no later than the contract
              timestamp plus 86400 seconds. The sentinel 0 is not
              permitted.</dd>
          <dt>PERMANENT:</dt>
          <dd>revocable; expiresAt <bcp14>MUST</bcp14> be the sentinel 0
              (no time bound).</dd>
          <dt>SYNDICATE:</dt>
          <dd>
            <t>revocable; intended for a fixed group of grantees that
               share one grant and lose access together, such as an
               incident-response team. expiresAt <bcp14>MAY</bcp14> be
               0 or a future timestamp. SYNDICATE differs from the other
               modes only in this intent and in its expiry rule.</t>
          </dd>
        </dl>
        <t>contractNonce is 16 random bytes.</t>
        <t>Any holder can name any holderId as a grantee, since
           holderIds are public. A contract therefore takes effect for a
           grantee only after that grantee has accepted it, by a means
           the operator provides outside this binding. An operator
           <bcp14>MAY</bcp14> treat contracts between holders it has
           linked in advance, such as holders of one organization, as
           accepted. A grantee <bcp14>MUST</bcp14> be able to decline or
           later withdraw its acceptance
           (<xref target="security-unsolicited"/>).</t>
        <t>A contract is active for a grantee while it is accepted by
           that grantee, not revoked, and its expiresAt is 0 or later
           than the current time.</t>
        <t>On saihm_recall by a grantee, the operator
           <bcp14>MUST</bcp14> include the covered cells of every active
           contract that names the grantee, and <bcp14>MUST NOT</bcp14>
           include a cell whose only grant to that grantee is a
           contract that is not active for that grantee.</t>
        <t>Revocation takes effect for every grantee of the contract as
           soon as the operator applies it
           (<xref target="effect-order"/>). It governs future reads
           only. Content a grantee has already received is outside its
           reach.</t>
      </section>

      <section anchor="erasure"><name>Erasure</name>
        <t>saihm_forget erases one cell. Every conformant operator
           <bcp14>MUST</bcp14>, on a successful erasure:</t>
        <ol>
          <li>stop serving the cell to the holder and to every grantee,
              under every contract;</li>
          <li>write a FORGET receipt for the cellId to the audit
              log;</li>
          <li>add the cellId to its erasure list, a public, append-only
              set of erased cellIds that matches the FORGET receipts in
              the audit log once any pending receipt is written
              (<xref target="effect-order"/>), so that any observer can
              rebuild it.</li>
        </ol>
        <t>An operator <bcp14>MUST</bcp14> consult the erasure list on
           every saihm_recall and <bcp14>MUST NOT</bcp14> return an
           erased cell to anyone.</t>
        <t>Beyond those steps an operator provides one of two erasure
           classes:</t>
        <dl>
          <dt>Cryptographic erasure:</dt>
          <dd>the operator also destroys every copy of the cell's
              wrapped DEK, including copies in backups and replicas.
              Because the DEK is random and wrapped only for this cell
              (<xref target="encryption-envelope"/>), the ciphertext
              then cannot be decrypted by anyone, the operator included,
              wherever copies of it remain.</dd>
          <dt>Access-control erasure:</dt>
          <dd>the cell is withdrawn from service as above, but the key
              material that could decrypt it is retained. The operator
              can still decrypt retained ciphertext.</dd>
        </dl>
        <t>Each operator <bcp14>MUST</bcp14> state in its published
           documentation which class it provides, and
           <bcp14>MUST NOT</bcp14> describe access-control erasure as
           cryptographic. Clients <bcp14>MUST NOT</bcp14> assert either
           class on the operator's behalf
           (<xref target="tool-forget"/>).</t>
        <t>Erasure does not remove the cell's receipts or its entry in
           the erasure list (<xref target="privacy-art17"/>).</t>
      </section>

      <section anchor="effect-order"><name>Order of effect and receipt</name>
        <t>For saihm_forget and saihm_revoke_share the operator
           <bcp14>MUST</bcp14> withdraw access before it writes the
           receipt, and <bcp14>MUST NOT</bcp14> restore access if the
           receipt write fails. If it cannot withdraw access it answers
           "operator_storage_unavailable" and nothing has changed. It
           retries the receipt until it is
           written. Until then it answers with the error
           "operator_log_unavailable", which here means that access has
           been withdrawn and the receipt is pending. A repeated
           saihm_revoke_share then returns revoked = true, and a repeated
           saihm_forget returns success = false (already erased). A
           client that receives this error <bcp14>SHOULD</bcp14> tell the
           agent that access may already be withdrawn and that repeating
           the call shows the outcome.</t>
        <t>For the other state-changing tools the operator first stores
           the effect durably but invisibly, then writes the receipt,
           then makes the effect visible. If the effect cannot be stored
           or the receipt cannot be written, the operator discards the
           effect (no cell is stored, no contract or proposal takes
           effect, no vote is counted) and answers
           "operator_storage_unavailable" or "operator_log_unavailable";
           for these tools both errors mean that no receipt was written
           and nothing took effect. If the operator cannot tell whether
           a receipt write succeeded, it <bcp14>MUST</bcp14> look the
           receipt up by its receiptId before answering, and treats a
           receipt it finds as written. Once the receipt is written the
           operator <bcp14>MUST</bcp14> complete the effect, including
           after a restart, and answers only with the success result
           carrying the result identifier.</t>
      </section>
    </section>

    <section anchor="operator-binding"><name>HTTPS binding</name>
      <section anchor="binding-request"><name>Request and response</name>
        <t>The client sends each tool call to the endpoint as one HTTP
           POST (<xref target="RFC9110"/>) with these headers:</t>
        <ul>
          <li>Content-Type: application/json</li>
          <li>Authorization: the operator-issued credential, sent
              verbatim.</li>
        </ul>
        <t>The request body is a JSON object with two members:</t>
        <artwork align="left">
{"method": "&lt;tool name&gt;", "params": { ... }}
        </artwork>
        <t>method is one of the eight tool names in
           <xref target="tool-surface"/>. params carries the operator
           parameters listed for that tool.</t>
        <t>On success the operator answers with a 2xx status and a
           JSON body that is the tool's result object (or, for
           saihm_recall, an array). There is no outer envelope. On
           failure it answers with a 4xx or 5xx status; it
           <bcp14>SHOULD</bcp14> include a JSON body
           {"error": "&lt;code&gt;"} using the codes listed per tool. An
           operator that does not enable a tool (for example, governance)
           <bcp14>MUST</bcp14> answer that tool with a failure
           status.</t>
        <t>A JSON integer that can exceed 2^53 - 1 (epochs, weights,
           counts) <bcp14>SHOULD</bcp14> be sent as a decimal
           string. A client <bcp14>MUST</bcp14> accept either a JSON
           number or a decimal string for every numeric field, and
           <bcp14>MUST</bcp14> accept true, false, "true", "false", 1,
           0, "1" and "0" for every boolean field.</t>
        <t>Fields not defined here are operator extensions. A client
           <bcp14>MUST</bcp14> ignore an extension it does not
           recognize, and an operator <bcp14>MUST NOT</bcp14> rely on a
           client displaying one.</t>
        <t>Hexadecimal values are base16 (<xref target="RFC4648"/>,
           Section 8) in lowercase. A cellId, holderId, contractId,
           proposalId or receiptId (including destructionAnchor) is 64
           hexadecimal characters.</t>
        <t>A client <bcp14>MAY</bcp14> report only the status code of a
           failure and not its body; the error codes below are for
           operators, logs and clients that read them.</t>
      </section>
      <section anchor="binding-auth"><name>Authentication</name>
        <t>The operator authenticates the caller from the Authorization
           header under a scheme it defines, for example a bearer token
           issued to one holder. The client treats the value as opaque
           and <bcp14>MUST NOT</bcp14> place private key material in
           it. The client does not sign requests; in the custodial
           profile the holder's signature on cells and receipts is made
           by the operator, after authentication, with the holder's
           custodied key.</t>
      </section>
      <section anchor="binding-transport"><name>Transport requirements</name>
        <t>A client:</t>
        <ul>
          <li><bcp14>MUST</bcp14> refuse an endpoint whose scheme is not
              https, except http to the host names 127.0.0.1 or
              localhost for local development.</li>
          <li><bcp14>MUST</bcp14> validate the server certificate as
              described in <xref target="RFC9525"/>.</li>
          <li><bcp14>MUST NOT</bcp14> follow HTTP redirects. A redirect
              is a failure. A followed 307 or 308 would resend the
              request body, which for saihm_remember is the memory
              plaintext, to a host the client never checked.</li>
          <li><bcp14>MUST</bcp14> bound each call with a timeout. The
              implementation in <xref target="appendix-implementation"/>
              uses 30 seconds. On timeout the client
              <bcp14>MUST</bcp14> report that the operator may or may
              not have acted.</li>
          <li><bcp14>MUST</bcp14> bound the size of a response while
              reading it, not only by its Content-Length header, which a
              chunked response omits. The implementation uses 16 MiB.</li>
          <li><bcp14>MUST NOT</bcp14> include the userinfo, query or
              fragment of the endpoint URL in any error text, including
              errors raised by its HTTP library, and
              <bcp14>MUST NOT</bcp14> quote an endpoint value that fails
              to parse as a URL. Error text reaches the model and its
              transcript, and any of these parts may hold a
              credential.</li>
          <li><bcp14>SHOULD</bcp14> report a 2xx response whose body is
              not JSON as a probable intermediary (a proxy or captive
              portal) and not as an operator result.</li>
        </ul>
      </section>
    </section>

    <section anchor="tool-surface"><name>Tool surface (MCP binding)</name>
      <t>The client exposes the protocol to the agent as eight MCP tools
         and no others:</t>
      <ul>
        <li>Core: saihm_remember, saihm_recall, saihm_forget,
            saihm_status.</li>
        <li>Sharing: saihm_share, saihm_revoke_share.</li>
        <li>Governance: saihm_governance_propose,
            saihm_governance_vote.</li>
      </ul>
      <t>The surface is fixed at eight within this revision. New contract modes, scopes or result fields
         are added by extending an existing tool's parameters or
         result. A future major revision <bcp14>MAY</bcp14> add or
         remove tools.</t>
      <t>Each tool below gives its MCP input, the operator parameters
         the client sends, the operator result, the checks the client
         applies before reporting success, and the MCP tool annotations
         <xref target="MCP"/> the client declares. The result of every
         state-changing tool can also carry receiptId
         (<xref target="audit-log"/>); a client that prints it applies
         <xref target="receipt-ids"/> to it.</t>

      <section anchor="receipt-ids"><name>Result identifiers</name>
        <t>cellId, contractId and proposalId are result identifiers. An
           operator returns them in the form given in
           <xref target="binding-request"/>. Whatever an operator sends,
           a client <bcp14>MUST</bcp14> reject any value that is not a
           JSON string of 1 to 256 characters in the range U+0021 to
           U+007E (printable ASCII, no space), wherever it would print
           the value, and <bcp14>MUST NOT</bcp14> truncate it.</t>
        <t>The client prints these values as structure in its report to
           the agent, for example "FORGOTTEN [id] erased". An
           identifier that contains a line break could add a second,
           forged line to the report. A truncated identifier cannot be
           passed back to saihm_forget or saihm_revoke_share.</t>
      </section>

      <section anchor="tool-remember"><name>saihm_remember</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>content: string (required).</dd>
          <dt>Operator parameters:</dt>
          <dd>{"content": string}</dd>
          <dt>Operator behavior:</dt>
          <dd>Authenticate the caller as a holder. Seal content into a
              new cell (<xref target="encryption-envelope"/>), sign it
              (<xref target="signed-messages"/>), store it, and write a
              REMEMBER receipt. The operator <bcp14>SHOULD</bcp14>
              reject content that is empty or only white space.</dd>
          <dt>Result:</dt>
          <dd>cellId (required); cellNonce; tier; kekVersion; epoch
              (the operator's time reference for the write);
              signaturePrefix (leading hex of the cell signature). An
              operator <bcp14>MAY</bcp14> add extension fields, such as
              accounting data.</dd>
          <dt>Client checks:</dt>
          <dd>The client <bcp14>MUST</bcp14> refuse, before sending,
              content that is empty or only white space. It
              <bcp14>MUST</bcp14> report the write as unconfirmed when
              the result is not an object or carries no valid cellId,
              since without one the memory cannot be erased later. It
              reports only the optional fields that are present.</dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint false, idempotentHint
              false, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"empty_content", "unauthenticated",
              "operator_storage_unavailable",
              "operator_log_unavailable", "rate_limited".</dd>
        </dl>
      </section>

      <section anchor="tool-recall"><name>saihm_recall</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>query: string (optional). Absent or empty means all
              cells.</dd>
          <dt>Operator parameters:</dt>
          <dd>{"query": string}. The member is omitted when the query
              is absent and is "" when the agent passed an empty
              string; the operator treats both as "all cells".</dd>
          <dt>Operator behavior:</dt>
          <dd>Select the caller's own cells and the covered cells of
              every active contract that names the caller with read
              access. Omit erased cells. Verify each cell's cellId and
              signature, and omit and log any cell that fails either
              check. Decrypt each remaining cell and apply the query as a
              filter on
              the plaintext. Matching is operator-defined; keyword
              containment is the baseline. Because the operator decrypts
              in this profile, the query is sent to it
              (<xref target="privacy-query"/>).</dd>
          <dt>Result:</dt>
          <dd>a JSON array. Each element is an object with cellId
              (required), plaintext (string, required), holderIdHex
              (required: the holderId of the cell's owner, which differs
              from the caller's for a shared cell), and optionally
              cellNonce, kekVersion, holderSignaturePrefix, timestamp and
              tier.</dd>
          <dt>Client checks:</dt>
          <dd>
            <t>The client <bcp14>MUST</bcp14> reject the recall as
               malformed if the result is not an array, or if any
               element is not a JSON object (null, a primitive and an
               array all fail).</t>
            <t>If the array is non-empty and no element carries a
               string plaintext, the endpoint
               returned sealed cells; the client <bcp14>MUST</bcp14>
               report that the operator is not custodial and that this
               client cannot read its memory. If some but not all lack
               plaintext, the client <bcp14>MUST</bcp14> report an
               incomplete recall, and <bcp14>MUST NOT</bcp14> attribute
               it to the custody model.</t>
            <t>The client marks cells owned by another holder as
               described in <xref target="client-conformance"/>.</t>
            <t>If any element lacks a valid cellId the client
               <bcp14>MUST</bcp14> reject the recall, since those cells
               could not be erased.</t>
            <t>An empty array after a non-empty query
               <bcp14>MUST NOT</bcp14> be reported as an empty store;
               the client says that nothing matched.</t>
          </dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint true, destructiveHint false, idempotentHint
              true, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"unauthenticated", "operator_storage_unavailable".</dd>
        </dl>
      </section>

      <section anchor="tool-forget"><name>saihm_forget</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>id: string, the cellId to erase (required).</dd>
          <dt>Operator parameters:</dt>
          <dd>{"id": string}</dd>
          <dt>Operator behavior:</dt>
          <dd>Confirm the caller holds the cell, then erase it
              (<xref target="erasure"/>). The cellId in the result is the
              one requested.</dd>
          <dt>Result:</dt>
          <dd>success (boolean, required). When true: cellId
              (required), and optionally destructionAnchor
              (the audit-log identifier of the FORGET receipt) and
              epoch. success false means the cell was not found or was
              already erased.</dd>
          <dt>Client checks:</dt>
          <dd>
            <t>An agent cannot confirm an erasure by later inspection,
               so the client reports it only on a complete result. The
               client <bcp14>MUST</bcp14>:</t>
            <ul>
              <li>report that the cell was not found or already erased
                  only when success is false;</li>
              <li>report the erasure as unconfirmed, and say the cell
                  may still be stored, when success is absent or not a
                  recognizable boolean;</li>
              <li>refuse to report an erasure when success is true but
                  cellId is missing or invalid;</li>
              <li>describe the effect without naming an erasure class,
                  which only the operator can state
                  (<xref target="erasure"/>).</li>
            </ul>
          </dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint true, idempotentHint
              true, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"unauthenticated", "not_holder",
              "operator_storage_unavailable",
              "operator_log_unavailable".</dd>
        </dl>
      </section>

      <section anchor="tool-status"><name>saihm_status</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>none.</dd>
          <dt>Operator parameters:</dt>
          <dd>{}</dd>
          <dt>Result:</dt>
          <dd>
            <t>an object. agentIdHashHex (the caller's holderId) is
               <bcp14>REQUIRED</bcp14>; a client that does not receive
               it reports the identity as not reported. A custodial
               operator
               <bcp14>SHOULD</bcp14> return custody = "custodial". The
               following fields are <bcp14>RECOMMENDED</bcp14>:</t>
            <dl>
              <dt>prs:</dt>
              <dd>number in [0.0, 1.0]. Process Reliability Score: the
                  fraction of tool calls answered within the operator's
                  published service window over a rolling 30-day
                  window.</dd>
              <dt>bfsi_window_start_ts, bfsi_R:</dt>
              <dd>
                <t>the start of the same window (UNIX epoch seconds) and
                   R, the number of receipts in the audit log attributed
                   to the holder in the window, which the holder can check
                   by listing its receipts in the log. From these a holder that
                   keeps its own log of tool calls computes the
                   receipt-match index:</t>
                <artwork align="left">
bfsi = 1 - (M / R)
                </artwork>
                <t>where M is the number of those receipts with no
                   matching call in the holder's log. When R = 0, bfsi
                   is 1.0 by convention, which signals no evidence of
                   unmatched receipts. The operator cannot see the
                   holder's log, so it <bcp14>MUST NOT</bcp14> return
                   bfsi or bfsi_M unless the holder has given it M by
                   means outside this binding. The client in
                   <xref target="appendix-implementation"/> keeps no
                   such log.</t>
              </dd>
              <dt>shards:</dt>
              <dd>an object keyed by tier name; each value is the number
                  of the holder's cells under that tier.</dd>
              <dt>contracts:</dt>
              <dd>an array of objects with contractId, mode
                  ("TEMPORARY", "PERMANENT" or "SYNDICATE"), granteeIds
                  (array of hex holderIds) and expiresAt (UNIX epoch
                  seconds; 0 for no bound). Expired entries
                  <bcp14>MAY</bcp14> be included; revoked contracts
                  <bcp14>MUST NOT</bcp14> be.</dd>
              <dt>governance:</dt>
              <dd>an array of objects with propId (the proposalId),
                  scope, opens_ts, closes_ts, tally_for, tally_against
                  and tally_abstain.</dd>
            </dl>
          </dd>
          <dt>Client checks:</dt>
          <dd>The client reports a field only when it is present and of
              usable type, and <bcp14>MAY</bcp14> summarize a list by its
              length. It <bcp14>MUST NOT</bcp14> substitute a default (a
              zero count, an empty epoch) for an absent field, and
              <bcp14>MUST NOT</bcp14> report an array that is absent as
              having length zero.</dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint true, destructiveHint false, idempotentHint
              true, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"unauthenticated".</dd>
        </dl>
      </section>

      <section anchor="tool-share"><name>saihm_share</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>granteeIdHashesHex: string[]; shardIds: string[]; type:
              "temporary" | "permanent" | "syndicate"; scope: "read" |
              "write" | "readwrite"; expiryEpoch: string (optional,
              decimal UNIX epoch seconds). All but expiryEpoch are
              required.</dd>
          <dt>Operator parameters:</dt>
          <dd>{"granteeIdHashesHex": string[], "shardIds": string[],
              "type": string, "scope": string, "expiryEpoch": string |
              null}. The client sends each grantee as lowercase hex
              without a "0x" prefix. type is the lowercase form of mode
              (<xref target="sharing-contracts"/>).</dd>
          <dt>Operator behavior:</dt>
          <dd>Confirm the caller holds every listed cell and that each
              grantee is a 32-byte holderId, apply the
              mode's expiry rule (a null expiryEpoch means 0 for
              PERMANENT and SYNDICATE; for TEMPORARY the operator
              applies its default within the 24-hour bound), sign and
              store the contract, and write a SHARE receipt.</dd>
          <dt>Result:</dt>
          <dd>contractId (required); type (the lowercase mode);
              granteeCount; epoch; operator extensions.</dd>
          <dt>Client checks:</dt>
          <dd>Before sending, the client <bcp14>MUST</bcp14> refuse an
              empty grantee list and an empty shard list; refuse any
              grantee that is not even-length hexadecimal (an optional
              "0x" prefix is allowed; length is checked by the
              operator), rather than decode it to a
              different identity; refuse a grantee listed twice, compared
              as bytes; and refuse an expiryEpoch that is not a string
              of decimal digits (an empty string means absent). After
              the call it <bcp14>MUST</bcp14> refuse to report a share
              without a valid contractId, and <bcp14>MUST</bcp14> flag a
              granteeCount that differs from the number requested.</dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint false, idempotentHint
              false, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"not_holder", "shard_not_found", "invalid_grantee",
              "operator_storage_unavailable",
              "invalid_mode", "invalid_scope", "invalid_expiry",
              "operator_log_unavailable".</dd>
        </dl>
      </section>

      <section anchor="tool-revoke-share"><name>saihm_revoke_share</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>contractId: string (required).</dd>
          <dt>Operator parameters:</dt>
          <dd>{"contractId": string}</dd>
          <dt>Operator behavior:</dt>
          <dd>Confirm the caller is the contract's holder, end access for
              every grantee, and write a REVOKE receipt
              (<xref target="effect-order"/>). Revoking a contract the
              caller has already revoked returns revoked = true, so a
              retry after a timeout reports the true state.</dd>
          <dt>Result:</dt>
          <dd>revoked (boolean, required); epoch.</dd>
          <dt>Client checks:</dt>
          <dd>Only revoked = true is a revocation. For any other value
              the client <bcp14>MUST</bcp14> lead its report with the
              fact that revocation was not confirmed and that access
              should be treated as still in force. The revocation applies
              to future reads only, and the cells themselves remain
              until erased.</dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint false, idempotentHint
              true, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"not_holder", "contract_not_found",
              "operator_storage_unavailable",
              "operator_log_unavailable".</dd>
        </dl>
      </section>

      <section anchor="tool-governance-propose"><name>saihm_governance_propose</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>scope: "emission_param" | "protocol_upgrade" (required);
              paramKey: string (optional); proposedValue: string
              (optional). "emission_param" proposes a new value for one
              parameter the operator publishes as governable; the name
              is kept for compatibility with deployed clients.
              "protocol_upgrade" proposes adopting a revision of the
              operator's implementation; it needs neither field, and the
              operator ignores both.</dd>
          <dt>Operator parameters:</dt>
          <dd>{"scope": string, "paramKey": string | null,
              "proposedValue": string | null}</dd>
          <dt>Operator behavior:</dt>
          <dd>Governance is <bcp14>OPTIONAL</bcp14> for operators, and
              an operator that has not enabled it answers with a failure
              status. Where enabled, the operator checks the caller
              against its eligibility rule
              (<xref target="governance-form"/>), records the proposal
              with a snapshot epoch, writes a PROPOSE receipt and opens
              the voting window.</dd>
          <dt>Result:</dt>
          <dd>proposalId (required); scope; paramKey; proposedValue;
              snapshotEpoch; proposerHash (the proposer's
              holderId).</dd>
          <dt>Client checks:</dt>
          <dd>Before sending, the client <bcp14>MUST</bcp14> refuse an
              emission_param proposal that lacks paramKey or
              proposedValue, since it would open a vote on no change.
              After the call it <bcp14>MUST</bcp14> refuse to report a
              proposal without a valid proposalId.</dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint false, idempotentHint
              false, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"governance_disabled", "ineligible_proposer",
              "unknown_scope", "operator_storage_unavailable",
              "operator_log_unavailable".</dd>
        </dl>
      </section>

      <section anchor="tool-governance-vote"><name>saihm_governance_vote</name>
        <dl>
          <dt>MCP input:</dt>
          <dd>proposalId: string (required); approve: boolean
              (required).</dd>
          <dt>Operator parameters:</dt>
          <dd>{"proposalId": string, "approve": boolean}</dd>
          <dt>Operator behavior:</dt>
          <dd>Where governance is enabled, check eligibility, compute the
              voter's weight at the proposal's snapshot epoch, record the
              vote, write a VOTE receipt and update the tally. A holder
              votes once per proposal. A repeated vote by the same holder,
              including one sent after the voting window has closed,
              changes nothing, writes no receipt, and returns the success
              result of the vote already recorded, with its own direction
              and weight. This
              binding records approve or
              reject; an operator with no other way to abstain reports
              tally_abstain as 0.</dd>
          <dt>Result:</dt>
          <dd>proposalId; voterHash (the voter's holderId); approve;
              weight; castAtEpoch.</dd>
          <dt>Client checks:</dt>
          <dd>
            <t>A failure status from the operator means the vote was
               not counted
               (<xref target="effect-order"/>). When there is no
               response or the body is malformed, the client reports
               that it is unknown whether the vote was counted. Sending
               the vote again does not count it twice, because the
               operator returns the vote already recorded, and the
               mismatch check below reports a direction that differs from
               the request. For a well-formed response the client reports, in
               order of precedence:</t>
            <ol>
              <li>unconfirmed, if neither weight nor castAtEpoch is
                  present (the other fields only echo the request);</li>
              <li>a mismatch, if the returned proposalId or approve
                  differs from the request;</li>
              <li>a zero-weight warning, if weight is 0;</li>
              <li>otherwise, the recorded vote.</li>
            </ol>
          </dd>
          <dt>Annotations:</dt>
          <dd>readOnlyHint false, destructiveHint false, idempotentHint
              false, openWorldHint true.</dd>
          <dt>Errors:</dt>
          <dd>"governance_disabled", "ineligible_voter",
              "proposal_not_found", "proposal_closed",
              "operator_storage_unavailable",
              "operator_log_unavailable".</dd>
        </dl>
      </section>

      <section anchor="governance-form"><name>Governance form</name>
        <t>This protocol defines the two governance tools but not a
           single governance form. An operator that enables governance
           <bcp14>MUST</bcp14> publish, at a stable URL, at least:</t>
        <ol type="(%c)">
          <li>eligibility to propose;</li>
          <li>eligibility to vote;</li>
          <li>the vote-weight function, evaluated at the proposal's
              snapshot epoch, so that weight acquired after the snapshot
              does not count;</li>
          <li>the threshold for a proposal to pass;</li>
          <li>the voting window, in seconds;</li>
          <li>the permitted proposal scopes and, for each, the
              parameters it may change.</li>
        </ol>
        <t>Tallies <bcp14>MUST</bcp14> be reproducible from the VOTE
           receipts in the audit log, counting, for each holder and
           proposal, only the first VOTE receipt in log order. Where two
           PROPOSE receipts carry the same proposalId, the first in log
           order governs.</t>
      </section>

      <section anchor="session-prompt"><name>Session prompt</name>
        <t>A client <bcp14>MAY</bcp14> also expose an MCP prompt, which
           is not a tool and does not change the eight-tool surface,
           that tells the agent to call saihm_recall at the start of a
           session and states when to store, recall, share and erase.
           The implementation in
           <xref target="appendix-implementation"/> names it
           saihm_session_bootstrap.</t>
      </section>
    </section>

    <section anchor="wire-formats"><name>Wire formats</name>
      <t>Tool calls and results travel as JSON
         (<xref target="operator-binding"/>). Cells and receipts at rest
         are CBOR (<xref target="RFC8949"/>). Timestamps in CBOR
         <bcp14>MUST</bcp14> be unsigned integers (major type 0).</t>
      <t>A cell at rest (CBOR diagnostic notation):</t>
      <artwork align="left">
{1: h'...cellId...',
 2: h'...holderId...',
 3: 1,
 4: "STANDARD",
 5: h'...cellNonce-16-bytes...',
 6: h'...ciphertext...',
 7: h'...signature...',
 8: 1747526400}
      </artwork>
      <t>Keys: 1=cellId, 2=holderId, 3=kekVersion, 4=tier,
         5=cellNonce, 6=ciphertext, 7=signature, 8=timestamp. The
         wrapped DEK is not part of the cell; the operator stores it
         separately, keyed by cellId, so that it can be destroyed
         without rewriting the cell.</t>
      <t>A receipt (CBOR diagnostic notation):</t>
      <artwork align="left">
{1: h'...receiptId...',
 2: h'...subjectId...',
 3: "REMEMBER",
 4: h'...holderId...',
 5: h'...signature...',
 6: 1747526400,
 8: h'...receiptNonce-16-bytes...'}
      </artwork>
      <t>Key 7, detail, is present only in KEY receipts (the holder's
         public key, as a byte string), PROPOSE receipts (a map
         {"scope": text, "paramKey": text / null, "proposedValue":
         text / null, "snapshotEpoch": uint}) and VOTE receipts (a map
         {"approve": bool, "weight": uint}).</t>
      <t>A sharing contract (CBOR diagnostic notation):</t>
      <artwork align="left">
{1: h'...contractId...',
 2: h'...holderId...',
 3: [h'...granteeId...'],
 4: [h'...cellId...'],
 5: "TEMPORARY",
 6: "read",
 7: 1747612800,
 8: 1747526400,
 9: h'...signature...',
 10: h'...contractNonce-16-bytes...'}
      </artwork>
      <t>Keys: 1=contractId, 2=holderId, 3=granteeIds, 4=shardIds,
         5=mode, 6=scope, 7=expiresAt, 8=timestamp, 9=signature,
         10=contractNonce.</t>
      <t>A saihm_status result (JSON):</t>
      <artwork align="left">
{"agentIdHashHex":       "ab4f746f...b1f588",
 "custody":              "custodial",
 "prs":                  0.997,
 "bfsi_window_start_ts": "1745020800",
 "bfsi_R":               "0",
 "shards":               {"STANDARD": 42},
 "contracts":            [
   {"contractId": "9c1e...",
    "mode":       "TEMPORARY",
    "granteeIds": ["5d02..."],
    "expiresAt":  "1747612800"}],
 "governance":           []}
      </artwork>
    </section>

    <section anchor="receipt-semantics"><name>Receipt semantics</name>
      <t>For each call that changes state (saihm_remember, saihm_forget,
         saihm_share, saihm_revoke_share, saihm_governance_propose,
         saihm_governance_vote) the operator <bcp14>MUST</bcp14> write
         a receipt to the audit log carrying the operation, the
         subjectId, the holderId and the timestamp, signed with the
         holder's key (<xref target="signed-messages"/>). A call that
         changes nothing, such as a repeated vote
         (<xref target="tool-governance-vote"/>) or a repeated
         saihm_forget or saihm_revoke_share, writes no receipt.</t>
      <t>A receipt <bcp14>MUST NOT</bcp14> contain plaintext, a DEK,
         a wrapped DEK, a KEK, the holder seed or a private key. A
         FORGET receipt carries the erased cellId as its subjectId.</t>
      <t>Read-only calls (saihm_recall, saihm_status)
         <bcp14>MAY</bcp14> produce receipts under operator policy,
         under the same content restrictions.</t>
    </section>

    <section anchor="client-conformance"><name>Client conformance</name>
      <t>A conformant custodial client:</t>
      <ol>
        <li>exposes exactly the eight tools of
            <xref target="tool-surface"/>, with the inputs and
            annotations given there;</li>
        <li>holds no key material and performs no cryptography beyond
            TLS;</li>
        <li>meets the transport requirements of
            <xref target="binding-transport"/>;</li>
        <li>treats every operator response as untrusted input: it checks
            the container type and each field's type before use, and
            reports a malformed response as malformed;</li>
        <li>reports saihm_remember, saihm_forget, saihm_share and
            saihm_governance_propose as complete only when the result
            carries the result identifier that names the object
            (<xref target="receipt-ids"/>), and reports
            saihm_revoke_share and saihm_governance_vote as described in
            <xref target="tool-revoke-share"/> and
            <xref target="tool-governance-vote"/>;</li>
        <li>reports an optional result field only when it is present
            and usable, and never prints in its place a placeholder
            that reads as a value, such as "undefined";</li>
        <li>never prints an operator-supplied string other than recall
            plaintext that contains a control character (U+0000 to
            U+001F, U+007F to U+009F), a line or paragraph separator
            (U+2028, U+2029) or a bidirectional control (U+202A to
            U+202E, U+2066 to U+2069) in its report, and either omits or
            rejects it; this includes any excerpt of a non-JSON response;</li>
        <li>presents recall plaintext so that a line break in it (CR,
            LF, VT, FF, U+0085, U+2028 or U+2029) cannot start a new
            report entry, for example by indenting every
            continuation line, and keeps the unaltered plaintext in its
            structured result;</li>
        <li>marks as shared by another holder, in its report and in its
            structured result, every recalled cell whose holderIdHex
            differs from the caller's holderId (compared without regard
            to letter case) or that lacks holderIdHex. The client learns
            the caller's holderId from agentIdHashHex in saihm_status;
            while it does not know it, it marks every cell. It
            <bcp14>SHOULD</bcp14> therefore call saihm_status before it
            reports a recall;</li>
        <li>applies the per-tool checks in
            <xref target="tool-surface"/>.</li>
      </ol>
      <t>The agent reads the client's report and acts on it, and a
         report of an erasure, a revocation or a vote that did not
         happen as reported is hard to discover afterwards.</t>
    </section>

    <section anchor="security"><name>Security considerations</name>
      <section anchor="security-custody"><name>Trust in a custodial operator</name>
        <t>In the custodial profile the operator holds every key and
           sees every plaintext. Encryption at rest protects against
           theft of stored ciphertext and of backups, and against a
           storage provider separate from the operator. It does not
           protect a holder's content from the operator itself. That
           protection requires a profile in which the holder keeps the
           keys.</t>
        <t>The holder's signatures are made by the operator. They
           establish that the operator, having authenticated the caller
           as that holder, wrote the cell or receipt. They do not prove
           that the holder's own machine produced it.</t>
        <t>The operator <bcp14>MUST</bcp14> protect the holder seeds,
           identity keys and KEKs it holds at least as strongly as the
           plaintext they protect, and <bcp14>SHOULD</bcp14> keep KEKs
           in hardware or a separate key service.</t>
      </section>
      <section anchor="security-pq"><name>Post-quantum signatures</name>
        <t>Cells and receipts are signed with ML-DSA-65
           (<xref target="FIPS204"/>), a NIST-standardized post-quantum
           signature algorithm, chosen so that receipts resist forgery by
           an adversary with a cryptographically relevant quantum
           computer. This does not
           extend to the operator's custody of the keys, or to the
           audit log's own integrity mechanism, whose algorithms are
           chosen by the deployment. TLS key exchange
           <bcp14>SHOULD</bcp14> include a post-quantum or hybrid group
           where both ends support one.</t>
      </section>
      <section anchor="security-credential"><name>Credential handling</name>
        <t>The Authorization value is a bearer credential for the
           holder's memory. A client <bcp14>MUST NOT</bcp14> echo it, or
           an endpoint value that may contain it, into tool output,
           including in errors raised by its HTTP library when the value
           is malformed (<xref target="binding-transport"/>). Operators
           <bcp14>SHOULD</bcp14> issue credentials scoped to one holder,
           with an expiry, and revocable.</t>
      </section>
      <section anchor="security-response"><name>Hostile or faulty responses</name>
        <t>A client cannot assume the endpoint is the operator. A
           misconfigured URL, a proxy or an attacker on a weakened
           transport can answer instead. The checks in
           <xref target="client-conformance"/> limit what such a
           responder can make the agent believe. In particular, the
           result-identifier rule (<xref target="receipt-ids"/>), the
           control-character rule (<xref target="client-conformance"/>)
           and the framing rule for recall plaintext
           (<xref target="client-conformance"/>) together keep a single
           response from adding a forged report entry,
           the erasure rule prevents an
           acknowledgement without an identifier from being reported as
           an erasure, and the redirect rule keeps plaintext from
           leaving for an unchecked host.</t>
      </section>
      <section anchor="security-unsolicited"><name>Unsolicited shares</name>
        <t>A sharing contract places another holder's text in the
           grantee's recall, which an agent loads at the start of a
           session. Under <xref target="sharing-contracts"/> a holder can
           do this only after the grantee has accepted the contract, or
           where the operator has linked the two holders in advance. Accepted shared text is still
           written by another holder. The client marks it
           (<xref target="client-conformance"/>), and an agent should not
           follow instructions found in it.</t>
      </section>
      <section anchor="security-erasure"><name>Erasure properties</name>
        <t>Cryptographic erasure holds only if no copy of the DEK, and
           nothing it can be derived from, survives. This is why the DEK
           is random and independent of identityKey
           (<xref target="encryption-envelope"/>). An operator that
           derived DEKs from a long-lived secret could re-derive a
           deleted DEK and decrypt, so it would provide access-control
           erasure at most.</t>
        <t>Plaintext the operator returned to the holder before erasure,
           and plaintext a grantee received, are outside the operator's
           control, and erasure does not affect them.</t>
      </section>
    </section>

    <section anchor="privacy"><name>Privacy considerations</name>
      <section anchor="privacy-art17"><name>GDPR Article 17</name>
        <t>saihm_forget gives a holder an erasure operation with a
           receipt in a public log and an entry in a public erasure
           list. Whether it also makes retained ciphertext unreadable
           depends on the erasure class the operator states
           (<xref target="erasure"/>). The protocol does not by itself
           make an operator compliant. Operators acting as controllers
           or processors under <xref target="GDPR"/> remain responsible
           for erasure without undue delay, including any plaintext in
           logs, caches and backups, and handle a request from a data
           subject who is not the holder outside this protocol.</t>
        <t>Erasure leaves the cell's receipts and its erasure-list entry
           in place. The receipts carry the holderId and the erasure-list
           entry carries the cellId. Before choosing a
           public log, an operator assesses whether holderIds are
           personal data in its deployment and, if so, uses separate
           holderIds per context or a log whose entries it can
           restrict.</t>
      </section>
      <section anchor="privacy-query"><name>Recall queries</name>
        <t>Because the operator decrypts, it also receives each recall
           query; draft-saihm-memory-protocol-01 kept the query on the
           holder's side. A query can reveal what the agent is working
           on even when it matches nothing. Operators
           <bcp14>SHOULD NOT</bcp14> log query text, and
           <bcp14>MUST NOT</bcp14> write it to a receipt.</t>
      </section>
      <section anchor="privacy-minimization"><name>Audit log content</name>
        <t>Receipts are public. They show that a holderId performed an
           operation on a subject at a time, but not memory content. The
           existence and timing of a holder's cells are therefore
           observable. PROPOSE receipts disclose the proposal, VOTE
           receipts disclose each vote's direction and weight, and an
           operator that writes RECALL or STATUS receipts discloses when
           the holder reads. Where that is sensitive, further measures outside
           this protocol apply, such as separate holder identities per
           context.</t>
      </section>
    </section>

    <section anchor="iana"><name>IANA Considerations</name>
      <t>This document has no IANA actions.</t>
    </section>
  </middle>

  <back>
    <references>
      <name>References</name>
      <references>
        <name>Normative References</name>
        <reference anchor="FIPS180-4" target="https://csrc.nist.gov/pubs/fips/180-4/upd1/final">
          <front>
            <title>Secure Hash Standard (SHS)</title>
            <author>
              <organization>National Institute of Standards and Technology</organization>
            </author>
            <date year="2015" month="August"/>
          </front>
          <seriesInfo name="FIPS" value="PUB 180-4"/>
        </reference>
        <reference anchor="FIPS204" target="https://csrc.nist.gov/pubs/fips/204/final">
          <front>
            <title>Module-Lattice-Based Digital Signature Standard</title>
            <author>
              <organization>National Institute of Standards and Technology</organization>
            </author>
            <date year="2024" month="August"/>
          </front>
          <seriesInfo name="FIPS" value="PUB 204"/>
        </reference>
        <reference anchor="IEEE-1003.1-2024" target="https://standards.ieee.org/ieee/1003.1/7700/">
          <front>
            <title>IEEE Standard for Information Technology--Portable Operating System Interface (POSIX(TM)) Base Specifications, Issue 8</title>
            <author>
              <organization>IEEE</organization>
            </author>
            <date year="2024" month="June"/>
          </front>
          <seriesInfo name="IEEE Std" value="1003.1-2024"/>
        </reference>
        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author initials="S." surname="Bradner" fullname="Scott Bradner"/>
            <date year="1997" month="March"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
          <seriesInfo name="DOI" value="10.17487/RFC2119"/>
        </reference>
        <reference anchor="RFC5116" target="https://www.rfc-editor.org/info/rfc5116">
          <front>
            <title>An Interface and Algorithms for Authenticated Encryption</title>
            <author initials="D." surname="McGrew" fullname="David McGrew"/>
            <date year="2008" month="January"/>
          </front>
          <seriesInfo name="RFC" value="5116"/>
          <seriesInfo name="DOI" value="10.17487/RFC5116"/>
        </reference>
        <reference anchor="RFC5869" target="https://www.rfc-editor.org/info/rfc5869">
          <front>
            <title>HMAC-based Extract-and-Expand Key Derivation Function (HKDF)</title>
            <author initials="H." surname="Krawczyk"/>
            <author initials="P." surname="Eronen"/>
            <date year="2010" month="May"/>
          </front>
          <seriesInfo name="RFC" value="5869"/>
          <seriesInfo name="DOI" value="10.17487/RFC5869"/>
        </reference>
        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author initials="B." surname="Leiba"/>
            <date year="2017" month="May"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
          <seriesInfo name="DOI" value="10.17487/RFC8174"/>
        </reference>
        <reference anchor="RFC8949" target="https://www.rfc-editor.org/info/rfc8949">
          <front>
            <title>Concise Binary Object Representation (CBOR)</title>
            <author initials="C." surname="Bormann"/>
            <author initials="P." surname="Hoffman"/>
            <date year="2020" month="December"/>
          </front>
          <seriesInfo name="STD" value="94"/>
          <seriesInfo name="RFC" value="8949"/>
          <seriesInfo name="DOI" value="10.17487/RFC8949"/>
        </reference>
      
        <reference anchor="MCP" target="https://modelcontextprotocol.io/">
          <front>
            <title>Model Context Protocol Specification</title>
            <author>
              <organization>Anthropic</organization>
            </author>
            <date year="2025" month="November" day="25"/>
          </front>
          <annotation>Donated to the Agentic AI Foundation under the Linux Foundation on 9 December 2025.</annotation>
        </reference>
        <reference anchor="RFC4648" target="https://www.rfc-editor.org/info/rfc4648">
          <front>
            <title>The Base16, Base32, and Base64 Data Encodings</title>
            <author initials="S." surname="Josefsson"/>
            <date year="2006" month="October"/>
          </front>
          <seriesInfo name="RFC" value="4648"/>
          <seriesInfo name="DOI" value="10.17487/RFC4648"/>
        </reference>
        <reference anchor="RFC8259" target="https://www.rfc-editor.org/info/rfc8259">
          <front>
            <title>The JavaScript Object Notation (JSON) Data Interchange Format</title>
            <author initials="T." surname="Bray"/>
            <date year="2017" month="December"/>
          </front>
          <seriesInfo name="STD" value="90"/>
          <seriesInfo name="RFC" value="8259"/>
          <seriesInfo name="DOI" value="10.17487/RFC8259"/>
        </reference>
        <reference anchor="RFC9846" target="https://www.rfc-editor.org/info/rfc9846">
          <front>
            <title>The Transport Layer Security (TLS) Protocol Version 1.3</title>
            <author initials="E." surname="Rescorla"/>
            <date year="2026" month="July"/>
          </front>
          <seriesInfo name="RFC" value="9846"/>
          <seriesInfo name="DOI" value="10.17487/RFC9846"/>
        </reference>
        <reference anchor="RFC9110" target="https://www.rfc-editor.org/info/rfc9110">
          <front>
            <title>HTTP Semantics</title>
            <author initials="R." surname="Fielding"/><author initials="M." surname="Nottingham"/><author initials="J." surname="Reschke"/>
            <date year="2022" month="June"/>
          </front>
          <seriesInfo name="STD" value="97"/>
          <seriesInfo name="RFC" value="9110"/>
          <seriesInfo name="DOI" value="10.17487/RFC9110"/>
        </reference>
        <reference anchor="RFC9525" target="https://www.rfc-editor.org/info/rfc9525">
          <front>
            <title>Service Identity in TLS</title>
            <author initials="P." surname="Saint-Andre"/><author initials="R." surname="Salz"/>
            <date year="2023" month="November"/>
          </front>
          <seriesInfo name="RFC" value="9525"/>
          <seriesInfo name="DOI" value="10.17487/RFC9525"/>
        </reference>
      </references>
      <references>
        <name>Informative References</name>
        <reference anchor="AAIF" target="https://aaif.io/">
          <front>
            <title>Agentic AI Foundation</title>
            <author>
              <organization>Linux Foundation</organization>
            </author>
            <date year="2025" month="December" day="9"/>
          </front>
        </reference>
        <reference anchor="GDPR" target="https://eur-lex.europa.eu/eli/reg/2016/679/oj">
          <front>
            <title>Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)</title>
            <author>
              <organization>European Parliament and Council</organization>
            </author>
            <date year="2016" month="April"/>
          </front>
        </reference>
        <reference anchor="RFC4846" target="https://www.rfc-editor.org/info/rfc4846">
          <front>
            <title>Independent Submissions to the RFC Editor</title>
            <author initials="J." surname="Klensin"/>
            <author initials="D." surname="Thaler"/>
            <date year="2007" month="July"/>
          </front>
          <seriesInfo name="RFC" value="4846"/>
          <seriesInfo name="DOI" value="10.17487/RFC4846"/>
        </reference>
        <reference anchor="RFC8730" target="https://www.rfc-editor.org/info/rfc8730">
          <front>
            <title>Independent Submission Editor Model</title>
            <author initials="N." surname="Brownlee"/>
            <author initials="R." surname="Hinden"/>
            <date year="2020" month="February"/>
          </front>
          <seriesInfo name="RFC" value="8730"/>
          <seriesInfo name="DOI" value="10.17487/RFC8730"/>
        </reference>
      
        <reference anchor="NIST-SP-800-38D" target="https://csrc.nist.gov/pubs/sp/800/38/d/final">
          <front>
            <title>Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC</title>
            <author initials="M." surname="Dworkin" fullname="Morris Dworkin"/>
            <date year="2007" month="November"/>
          </front>
          <seriesInfo name="NIST" value="SP 800-38D"/>
        </reference>
        <reference anchor="RFC4086" target="https://www.rfc-editor.org/info/rfc4086">
          <front>
            <title>Randomness Requirements for Security</title>
            <author initials="D." surname="Eastlake 3rd"/><author initials="J." surname="Schiller"/><author initials="S." surname="Crocker"/>
            <date year="2005" month="June"/>
          </front>
          <seriesInfo name="BCP" value="106"/>
          <seriesInfo name="RFC" value="4086"/>
          <seriesInfo name="DOI" value="10.17487/RFC4086"/>
        </reference>
        <reference anchor="RFC7942" target="https://www.rfc-editor.org/info/rfc7942">
          <front>
            <title>Improving Awareness of Running Code: The Implementation Status Section</title>
            <author initials="Y." surname="Sheffer"/><author initials="A." surname="Farrel"/>
            <date year="2016" month="July"/>
          </front>
          <seriesInfo name="BCP" value="205"/>
          <seriesInfo name="RFC" value="7942"/>
          <seriesInfo name="DOI" value="10.17487/RFC7942"/>
        </reference>
      </references>
    </references>


    <section anchor="appendix-implementation" numbered="true" toc="default">
      <name>Implementation status</name>
      <t>This appendix follows the practice of
         <xref target="RFC7942"/>. It records an implementation at the
         time of writing and is to be removed before publication as an
         RFC.</t>
      <dl>
        <dt>Name:</dt>
        <dd>@saihm/mcp-server, version 0.3.17, published on the npm
            registry.</dd>
        <dt>License:</dt>
        <dd>Apache 2.0.</dd>
        <dt>Role:</dt>
        <dd>custodial client. It implements the eight tools of
            <xref target="tool-surface"/>, the HTTPS binding of
            <xref target="operator-binding"/>, the client checks of
            <xref target="client-conformance"/>, and the session prompt
            of <xref target="session-prompt"/>. It is configured with
            two environment variables, SAIHM_ENDPOINT_URL and
            SAIHM_AUTH_HEADER. It holds no key material, and its eight
            tools perform no cryptography beyond TLS.</dd>
        <dt>Coverage:</dt>
        <dd>all eight tools. The client's status report also shows
            operator extension fields when an operator returns them.</dd>
        <dt>Known deviations:</dt>
        <dd>
          <t>In version 0.3.17:</t>
          <ul>
            <li>an endpoint URL that carries userinfo, and an
                Authorization value that contains a line break or NUL,
                are quoted in full in errors raised by the HTTP library
                (<xref target="security-credential"/>);</li>
            <li>two places print an operator-supplied identifier,
                and saihm_forget prints destructionAnchor shortened to 32
                characters, without the check of
                <xref target="receipt-ids"/>;</li>
            <li>other printed fields, the HTTP reason phrase, recall
                plaintext and the excerpt of a non-JSON response are
                printed without the
                control-character and framing rules, and cells shared
                by another holder are not marked
                (<xref target="client-conformance"/>);</li>
            <li>after "operator_log_unavailable" from saihm_forget or
                saihm_revoke_share it reports only the failure status,
                without the advice of <xref target="effect-order"/>;</li>
            <li>its message for an unconfirmed vote advises against
                sending the vote again, and a vote answered with a 2xx
                body that is not JSON is not reported as of unknown
                outcome
                (<xref target="tool-governance-vote"/>);</li>
            <li>an absent agentIdHashHex appears as an empty string in
                the structured result
                (<xref target="tool-status"/>).</li>
          </ul>
          <t>The first three are to be corrected in the next
             release.</t>
        </dd>
        <dt>Contact:</dt>
        <dd>architect@saihm.net</dd>
      </dl>
    </section>

    <section anchor="appendix-worked-example" numbered="true" toc="default">
      <name>Worked example</name>
      <section anchor="appendix-seq-diagram"><name>Lifecycle</name>
        <artwork align="left"><![CDATA[
 Agent + client             Operator                  Audit log
 ==============             ========                  =========
      |                         |                          |
      | POST saihm_remember     |                          |
      | {content}, Authz        |                          |
      |------------------------>| new DEK, nonce; seal;    |
      |                         | sign as holder; store    |
      |                         |------ REMEMBER --------->|
      |  {cellId, ...}          |                          |
      |<------------------------|                          |
      |                         |                          |
      | POST saihm_recall       |                          |
      |------------------------>| verify, decrypt, filter  |
      |  [{cellId, plaintext}]  |                          |
      |<------------------------|                          |
      |                         |                          |
      | POST saihm_share        |                          |
      |------------------------>| sign contract            |
      |                         |------ SHARE ------------>|
      |  {contractId, ...}      |                          |
      |<------------------------|                          |
      |                         |                          |
      | POST saihm_revoke_share |                          |
      |------------------------>|------ REVOKE ----------->|
      |  {revoked: true}        |                          |
      |<------------------------|                          |
      |                         |                          |
      | POST saihm_forget {id}  |                          |
      |------------------------>| withdraw; list cellId;   |
      |                         | destroy wrapped DEK *    |
      |                         |------ FORGET ----------->|
      |  {success, cellId,      |                          |
      |   destructionAnchor}    |                          |
      |<------------------------|                          |

 * cryptographic erasure class only (Section 2.7)
]]></artwork>
      </section>

      <section anchor="appendix-crypto-values"><name>Example values</name>
        <t>These values come from a labeled test seed and
           <bcp14>MUST NOT</bcp14> be used for real cells. They can be
           reproduced with any FIPS 204 ML-DSA-65, RFC 5869 HKDF and
           AES-256-GCM implementation.</t>
        <t>Test holder seed, SHA-256 of the 46-byte ASCII string given
           here on one line:</t>
        <artwork align="left">
SAIHM-TEST-VECTOR-001-DO-NOT-USE-IN-PRODUCTION
        </artwork>
        <artwork align="left">
holderSeed =
  f068b8db8484d33bdbedd154bf5bf28e11fba330b79469e23595d6f738d7f5c6
        </artwork>
        <t>Identity key (<xref target="identity-binding"/>):</t>
        <artwork align="left">
identityKey =
  fdf786da8cb1f074393f9ad6dec1671e08085540e95c8463c9f2e15f437bbc5a
  2c267aeaf355e79d8968cc21846cdff7e16f498437de3d6b1fd290703eeed9c2
        </artwork>
        <t>holderId, SHA-256 of the ML-DSA-65 public key generated from
           the first 32 bytes of identityKey:</t>
        <artwork align="left">
holderId =
  ab4f746fd1520d2736854559d6751969ae9127f5dbc607d7298acbf1afb1f588
        </artwork>
        <t>kekVersion = 1. cellNonce and DEK, which in operation come
           from the random source, are fixed here so the example can be
           reproduced:</t>
        <artwork align="left">
cellNonce = 25bd74b827789faacad8ffb7593c2359
DEK       =
  a1e54f730c6c7a1048ae436c9d2b179821c6eddd7841f41e36215285f3ffd07a
        </artwork>
        <t>The 41-byte plaintext, in hexadecimal (ASCII "Hello, SAIHM.",
           one space, "This is a test memory cell."), is encrypted with
           AES-256-GCM, IV = first 12 bytes of cellNonce, no associated
           data:</t>
        <artwork align="left">
plaintext =
  48656c6c6f2c20534149484d2e205468697320697320612074657374206d656d
  6f72792063656c6c2e
        </artwork>
        <artwork align="left">
ciphertext (57 bytes, including 16-byte tag) =
  595b0b9f77a8d95f29c5affa151bbcc18fdc3f7e5223792627919ee2d52f2bf2
  4933c35de4c1755559bee818ef54b48a388bcdd2e4a30fbde8
        </artwork>
        <t>cellId (<xref target="cell-shape"/>):</t>
        <artwork align="left">
cellId =
  d851960a5b7754c5884c96bef5d615e666c8ad006e4ceebe028cd85aae8e7c2f
        </artwork>
        <t>timestamp = 1747526400 (2025-05-18T00:00:00Z). Cell
           signature (<xref target="signed-messages"/>): deterministic
           ML-DSA-65 with context "SAIHM-CELL-v1". It is 3309 bytes; the
           first 32 are:</t>
        <artwork align="left">
signature[0..32) =
  8b7466cd59b30c38cdacdf2a55fff14ba39dff905ad20a3fdffe9018f5bacf38
        </artwork>
        <t>A hedged signature verifies the same way but is not
           reproducible.</t>
      </section>

      <section anchor="appendix-cbor-walkthrough"><name>CBOR encoding</name>
      <artwork align="left"><![CDATA[

A8                # map(8)
   01             #   key 1 (cellId)
   58 20          #   bytes(32)
    d851960a5b7754c5884c96bef5d615e666c8ad006e4ceebe028cd85aae8e7c2f
   02             #   key 2 (holderId)
   58 20          #   bytes(32)
    ab4f746fd1520d2736854559d6751969ae9127f5dbc607d7298acbf1afb1f588
   03             #   key 3 (kekVersion)
   01             #   unsigned(1)
   04             #   key 4 (tier)
   68             #   text(8)
    5354414e44415244            # "STANDARD"
   05             #   key 5 (cellNonce)
   50             #   bytes(16)
    25bd74b827789faacad8ffb7593c2359
   06             #   key 6 (ciphertext)
   58 39          #   bytes(57)
    595b0b9f77a8d95f29c5affa151bbcc18fdc3f7e5223792627919ee2d52f2bf2
    4933c35de4c1755559bee818ef54b48a388bcdd2e4a30fbde8
   07             #   key 7 (signature)
   59 0CED        #   bytes(3309)
    8b7466cd59b30c38cdacdf2a55fff14ba39dff905ad20a3fdffe9018f5bacf38
    ... (3277 further bytes)
   08             #   key 8 (timestamp)
   1A 68292300    #   unsigned(1747526400)
]]></artwork>
      </section>
    </section>

    <section anchor="appendix-changes" numbered="true" toc="default">
      <name>Changes from draft-saihm-memory-protocol-01</name>
      <ul>
        <li>Specifies the custodial profile: the operator holds the keys
            and performs all cryptography; the client holds none.</li>
        <li>Adds the HTTPS binding between client and operator, and
            removes per-call request signing by the client.</li>
        <li>Replaces the HKDF-derived DEK with a random per-cell DEK
            wrapped under the KEK, so that an operator holding the
            identity key can offer cryptographic erasure.</li>
        <li>Sends the recall query to the operator, which decrypts and
            filters.</li>
        <li>Reports a missing or erased cell in saihm_forget as
            success = false, makes revocation idempotent, and revises the
            error codes.</li>
        <li>Defines the signed messages with ML-DSA context strings,
            adds KEY receipts carrying the holder's public key, and
            defines the contract encoding.</li>
        <li>Restricts TEMPORARY expiry, defines shardIds as cellIds,
            reserves the write scopes, and states the order of effect and
            receipt for every state-changing tool.</li>
        <li>Moves the bfsi computation to the holder.</li>
        <li>Requires grantee acceptance before a contract takes
            effect, the owner's holderId on every recalled cell, and the
            marking of cells shared by another holder.</li>
        <li>Defines two erasure classes and requires the operator to
            state which it provides.</li>
        <li>Brings each tool's inputs and results into line with the
            implementation in <xref target="appendix-implementation"/>:
            share takes grantee and shard lists, a lowercase type and an
            access scope; vote takes a boolean; governance is optional
            for operators.</li>
        <li>Adds result identifiers, client conformance checks, and
            MCP tool annotations.</li>
        <li>Defines holderId as SHA-256 of the ML-DSA-65 public key, and
            the key-generation seed as the first 32 bytes of
            identityKey.</li>
        <li>Generalizes the audit anchor to a public append-only audit
            log, and leaves the storage tier and log to the
            deployment.</li>
        <li>Replaces the example signature prefix with a deterministic,
            reproducible value, and gives the example inputs in
            hexadecimal.</li>
      </ul>
    </section>
  </back>
</rfc>
