Asking and sharing architecture
Most interactions between people are inquiries about personal memory: availability, shared experiences, contact information, photos, or documents. Traditional cloud architectures force users to choose between two undesirable extremes:
- All-or-nothing data sharing: Sharing entire folders, photo albums, or calendars on commercial cloud platforms, permanently handing raw data to third parties.
- High-friction manual interruption: Messaging the person directly, waiting for them to search their personal devices, and receiving unverified, manual replies.
Companion Memory introduces interpersonal federated querying: Companions can ask bounded questions of other Companions and return truthful, minimally disclosed answers—without either party transferring their raw records, without centralized cloud brokers, and with the responder remaining in absolute cryptographic control of disclosure.
For a screen-by-screen walkthrough of how Asks works in the Companion Memory interface today, see Asking other Companions.
The two sharing models
Companion Memory provides two distinct sharing mechanisms designed for different threat models and operational needs:
flowchart TD
subgraph Passive["Passive sharing: Shared searches"]
SS["Saved search definition"] --> MintToken["Mint capability token"]
MintToken --> ScopedURL["Scoped URL / token"]
ScopedURL --> ReadOnlyClient["External browser / visitor (read-only)"]
end
subgraph Active["Active federation: Federated asks"]
Asker["Asker Companion (Local or Remote)"] --> RFC9421["Signed RFC 9421 HTTP Ask"]
RFC9421 --> ConsentGate["Consent and disclosure gate"]
ConsentGate --> LocalSearch["Local search (runs as responder)"]
LocalSearch --> CeilingClamp["Disclosure ceiling clamp"]
CeilingClamp --> AskerPoll["Filtered answer returned via poll"]
end1. Shared searches (passive, scoped egress)
A Shared Search is a pre-authorized, read-only window into a specific slice of memory.
- Query-pinned: The search terms, date boundaries, and entity filters are fixed at creation time. The holder of the link cannot modify the query.
- Dynamic evaluation: Unlike a static export, when new records matching the search criteria are ingested, the view updates automatically.
- Scoped capability tokens: Access is gated by a cryptographically random capability token. The token can carry an expiration timestamp or be revoked immediately.
- Use case: Sharing receipts from a specific trip with an accountant, or sharing family vacation photos with relatives who do not run a Companion.
2. Federated asks (active, bilateral query-answer)
An Ask is an active question sent from one person’s Companion to another person’s Companion.
- No raw data egress: Raw database records never leave the responder’s appliance.
- Review by default: Unless covered by an explicit, pre-existing standing grant, every incoming question queues for manual owner review.
- Ceiling-clamped output: Answers are distilled to the lowest necessary rung of disclosure (for example, a yes/no boolean or an item count) before leaving the appliance.
- Use case: Asking a colleague for availability, asking a friend for photos of a specific event, or asking a peer for contact recommendations.
Closed query-type grammar
To prevent arbitrary SQL or prompt-injection queries from crossing trust boundaries, Companion Memory validates every ask against a closed grammar (askGrammar.ts and peers/protocol.ts):
Query type (queryType / archetype) | Composer prompt in /asks | Answer form | Built-in ceiling | Accepted on /agent/v1/ask peer wire? |
|---|---|---|---|---|
relationship | Do you know…? | boolean | boolean (Rung 1) | Yes (relationshipAskSchema) |
content | Do you have any…? / Do you have photos of…? | existence | count (Rung 2) | Yes (contentAskSchema) |
availability | Are you free…? | attribute (free/busy) | summary (Rung 3) | Yes (availabilityAskSchema) |
attribute | What is…? | attribute | summary (Rung 3) | Local / DCQL only |
identity | (API / DCQL only) | identity | summary (Rung 3) | Local / DCQL only |
Any request naming an unknown queryType or archetype is rejected before consent rules or search evaluation run.
Trust Over IP (ToIP) dual-stack implementation
Federated asks in Companion Memory follow the Trust Over IP (ToIP) 4-layer dual-stack model. This structure separates technical trust (cryptographic verification) from human trust (consent governance).
flowchart TB
subgraph GovernanceStack["Human & Governance Stack"]
direction TB
G4["Human consent governance<br/>Owner review, 6-rung disclosure ladder, audit ledger"]
G3["Standing grant credentials<br/>ACDC verifiable grants, Transaction Event Logs (TEL)"]
G2["Peer trust relationships<br/>Trust tiers: family, trusted, peer, stranger, household"]
G1["Appliance and person governance<br/>Decentralized identifiers, local authority"]
end
subgraph TechStack["Technical Trust Stack"]
direction TB
T4["Application protocols<br/>GraphQL /asks API, MCP agent tools, explorer UI"]
T3["Verifiable credential exchange<br/>Authentic Chained Data Containers (ACDC), SAID digests"]
T2["P2P communications<br/>RFC 9421 HTTP Message Signatures over /agent/v1/ask, OOBI"]
T1["Utility & root of trust (KERI)<br/>Autonomic Identifiers (AIDs), Key Event Logs (KELs), Pre-rotation"]
end
G4 <--> T4
G3 <--> T3
G2 <--> T2
G1 <--> T1Layer 1: Utility and root of trust (KERI)
Companion Memory does not rely on centralized Certificate Authorities (CAs), Domain Name System (DNS) records, or distributed blockchain ledgers for identity:
- Autonomic Identifiers (AIDs): Cryptographically self-certifying identifiers (
did:keri:<aid>). The identifier is derived directly from the inception event public key. - Key Event Logs (KEL): An append-only, tamper-evident hash chain of key state events (
icpinception,rotrotation, andixninteraction). - Pre-rotation commitments: Every key rotation commits to the cryptographic hash of the next public key (
nxt). If an active private key is compromised, the attacker cannot forge a rotation event because they do not control the pre-committed key pair.
Layer 2: Peer-to-peer protocols and secure interconnect
Companions communicate directly over HTTPS, Tailscale tailnets, or local networks:
- Out-of-Band Introductions (OOBI) and peer directory: P2P discovery URLs (
/.well-known/keri/oobi) allow two appliances to establish pairwise cryptographic links via QR codes or shared links without cloud mediation. WhenCI_PEER_DIRECTORY_URLis configured, appliances can also resolve handles through the Companion Portal directory (POST /api/peers,GET /api/peers/resolve/:handle,GET /api/peers/search,GET /api/peers/mine, andDELETE /api/peers/:handle), while inbound cross-box asks verify the responder’s audience (CI_PEER_AUDIENCE). - RFC 9421 HTTP Message Signatures: Every inbound ask and poll request to
/agent/v1/askmust carry an RFC 9421 signature over:@method(HTTP verb)@target-uri(Canonical URI reconstructed from server config)content-digest(SHA-256 digest of the request body)
- Nonce tracking and replay defense: A memory-bounded sliding window cache (
PeerSignatureService) tracks signature nonces to reject replayed requests.
Layer 3: Trust exchange and credentials (ACDC and TEL)
Standing permissions are represented as Authentic Chained Data Containers (ACDC):
- Self-Addressing Identifiers (SAID): Every grant object is uniquely identified by the cryptographic hash of its canonical content (
said.ts). - Transaction Event Logs (TEL): Revocation is tracked in an append-only event log (
ississuance andrevrevocation events). Companions can immediately verify credential validity without querying a centralized revocation list.
Layer 4: Human governance and consent logic
The top layer enforces owner autonomy and privacy boundaries:
- The six-rung disclosure ladder: Restricts the fidelity of every response.
- Trust tiers: Sets global maximum disclosure ceilings based on human relationships.
- Cryptographic audit logging: Every disclosure is written to an immutable audit ledger anchored by a SAID digest.
The six-rung disclosure ladder
When a question matches records in your memory, the answer is not returned as raw database rows. Instead, it is synthesized to the specific rung permitted by policy:
flowchart TD
R0["Rung 0: none<br/>No answer returned; indistinguishable from no matches"] --> R1["Rung 1: boolean<br/>Single bit: yes or no"]
R1 --> R2["Rung 2: count<br/>Magnitude / existence only, zero entities or names"]
R2 --> R3["Rung 3: summary<br/>One synthesized sentence, no item identifiers or dates"]
R3 --> R4["Rung 4: redacted<br/>Item references with sensitive attributes removed"]
R4 --> R5["Rung 5: full<br/>Complete item references and entity metadata"]| Code rung | Label in /asks UI | What the asker receives | Example output |
|---|---|---|---|
none (0) | Nothing | No answer at all | delivered (Waiting for them in /asks; asker sees only that the request was received) |
boolean (1) | Just yes or no | A single verification bit | true or false |
count (2) | Just how many | Match magnitude or existence | 3 matching items found (or existence confirmation in /asks) |
summary (3) | A summary only | A short synthesized statement | "You were in Lake Tahoe together in July 2024." |
redacted (4) | Only what I pick | Item references with sensitive fields scrubbed | Photo IDs with location data and background faces stripped |
full (5) | Everything that matched | Full item references and metadata | Complete photo references, coordinates, and participant IDs |
Trust tiers and peer links
Every peer Companion linked to your appliance is assigned a Trust Tier. Trust tiers establish a permanent, non-negotiable ceiling on what that peer may receive:
flowchart LR
Stranger["stranger<br/>Max: boolean"] --> PeerTier["peer<br/>Max: summary"]
PeerTier --> Trusted["trusted<br/>Max: redacted"]
Trusted --> Family["family<br/>Max: full"]
subgraph InternalBoxOnly["Same appliance only"]
Household["household<br/>Max: full (exempt from sensitive floor)"]
end| Trust Tier | Where it applies | Maximum allowed rung | Purpose |
|---|---|---|---|
stranger | Default for unlinked peers | boolean | Restricts untrusted callers to binary availability checks. |
peer | Standard acquaintances | summary | Allows high-level confirmations without item references. |
trusted | Close friends and colleagues | redacted | Permits itemized answers with sensitive metadata redacted. |
family | Immediate family | full | Permits full item disclosures when authorized by a grant. |
household | Same appliance only | full | Internal accounts on the same physical server. |
The household boundary cannot cross networks. The household tier is
restricted to local multi-user accounts on the same machine. It is rejected by
database constraints (PEER_TRUST_TIERS) for external peers to prevent remote
appliances from inheriting local administrative privileges.
The five architectural invariants
Companion Memory enforces five non-negotiable architectural invariants (documented as Architectural Decision Records ADR-0001 through ADR-0005). These invariants guarantee that bugs or adversarial requests cannot breach user privacy.
ADR-0001: The asker sees only answered or delivered
To prevent attackers from probing whether you are ignoring them, whether a grant exists, or whether an ask was declined:
- The asker-visible status is folded down to exactly two states:
answeredordelivered(shown in/asksas Answered or Waiting for them). - Terminal outcomes such as
declined,expired, orwithheldare stored asdeliveredon the asker’s appliance (status: "pending", reason: "No answer is available"on/agent/v1/askpoll responses, folded intodeliveredbyaskerVisibleStatus). - The asker cannot differentiate between an ask waiting in your review queue and an ask you deliberately declined.
flowchart LR
subgraph ResponderBox["Responder internal state"]
Answered["answered"]
AutoAnswered["auto_answered"]
Declined["declined"]
Expired["expired"]
Pending["pending_review"]
end
subgraph AskerWire["Wire protocol / Asker view"]
DeliveredWire["delivered (Waiting for them)"]
AnsweredWire["answered (Answered)"]
end
Answered --> AnsweredWire
AutoAnswered --> AnsweredWire
Declined --> DeliveredWire
Expired --> DeliveredWire
Pending --> DeliveredWireADR-0002: Disclosure ceilings are stamped at consent time
A major vulnerability in authorization systems is the “revocation race”: revoking a restrictive grant might cause a request to fall back to a broader policy.
- Companion Memory stamps the active disclosure ceiling on the ask at the moment consent is granted.
- When the answer is computed, the engine evaluates both the stamped ceiling and the current policy, and applies whichever is narrower:
Effective Ceiling = min(Stamped Ceiling, Current Policy) - Revoking a grant can never broaden an answer; it can only narrow or eliminate it.
ADR-0003: Asynchronous polling via correlation IDs (no webhooks)
Companion Memory forbids webhook callbacks for federated ask replies:
- Webhook callbacks turn receiving boxes into open egress servers, making them vulnerable to Distributed Denial of Service (DDoS) and Server-Side Request Forgery (SSRF) amplification.
- All federated asks use a pull model: the asker mints a client-side
askId(correlation ID) and pollsGET /agent/v1/ask/:correlation. - Polling requests are rate-limited independently of the ask intake queue.
ADR-0004: Zero redirects and connect-time IP pinning
When fetching answers or dispatching asks to peer endpoints:
- The egress fetcher (
peerEgressFetch) forbids HTTP redirects (301, 302, 307, and 308). Following redirects would allow malicious peers to redirect an egress request to internal endpoints (for example,http://169.254.169.254orhttp://localhost:5002). - DNS resolution is performed once, and the target IP is pinned at socket connection time. This neutralizes DNS rebinding attacks.
ADR-0005: Null dataClass is wildcard for block, never for auto-answer
Requests and grants can optionally specify a dataClass (for example, photos, calendar, or health):
- In a block rule (
grant-block/mode: block), an omitted or nulldataClassacts as a wildcard: it blocks every data class. - In an auto-answer rule (
grant-auto/mode: auto), an omitted or nulldataClassis forbidden from acting as a wildcard: an auto-answer grant must explicitly name the specific data class it applies to.
End-to-end federated ask lifecycle
The following sequence diagram illustrates the complete execution path of a federated ask crossing between two sovereign appliances:
sequenceDiagram
autonumber
actor Alice as Alice (Asker)
participant BoxA as Alice's Companion
participant BoxB as Bob's Companion
actor Bob as Bob (Responder)
Alice->>BoxA: Compose Ask ("Free Friday lunch?")
BoxA->>BoxA: Create outbound Ask row & mint correlationId
BoxA->>BoxA: Sign request with KERI AID (RFC 9421)
BoxA->>BoxB: POST /agent/v1/ask (Signed headers, content-digest)
BoxB->>BoxB: Verify RFC 9421 signature & check replay nonce
BoxB->>BoxB: Resolve peer link for Alice's AID
BoxB->>BoxB: Check standing grant (Bob has "trusted: boolean" grant)
alt Standing grant exists (Auto-answer)
BoxB->>BoxB: Run local calendar search
BoxB->>BoxB: Clamp disclosure to boolean (true)
BoxB->>BoxB: Compute and record SAID audit hash
BoxB-->>BoxA: 200 OK (status: answered, answer: true)
BoxA->>Alice: Display "Free on Friday"
else No standing grant (Review required)
BoxB->>BoxB: Queue in Bob's "For you" inbox
BoxB-->>BoxA: 202 Accepted (status: delivered)
BoxB->>Bob: Display review card in UI
Bob->>BoxB: Approve with summary rung
BoxB->>BoxB: Run local search, clamp, and write answer
BoxB->>BoxB: Compute and record SAID audit hash
loop Poll until answered
BoxA->>BoxB: GET /agent/v1/ask/:correlation (Signed)
BoxB-->>BoxA: 200 OK (status: answered, summary answer)
end
BoxA->>Alice: Display summarized answer
endVerifiable audit records (SAID)
Every terminal outcome (answered, auto-answered, declined, or expired) generates an immutable audit record in ci.ask_audit.
To provide cryptographic non-repudiation between sovereign peers, Companion Memory computes a Self-Addressing Identifier (SAID) over the canonical JSON representation of each audit record:
// auditSaid.ts
export function computeAuditSaid(record: AuditableRecord): string {
const facts = {
askId: record.askId,
askerUserId: record.askerUserId,
peerLinkId: record.peerLinkId,
peerHandle: record.peerHandle,
responderUserId: record.responderUserId,
query: record.query,
consentBasis: record.consentBasis,
outcome: record.outcome,
disclosedCount: record.disclosedCount,
disclosedEntities: record.disclosedEntities,
d: "############################################", // 44-char placeholder
};
const canonical = canonicalStringify(facts); // RFC 8785 deterministic ordering
return computeCesrSha256Digest(Buffer.from(canonical, "utf8")); // Returns 'E...'
}If either party disputes what was asked or disclosed, the SAID can be recomputed against the audit payload. Any modification to query parameters, disclosed entities, or counts produces a mismatched digest, providing mathematical proof of tampering.
Related
- Asking other Companions — screen-by-screen guide to
/asks, standing grants, trust tiers, and what works today - Decentralized trust (KERI and ToIP) — autonomic identifiers, Key Event Logs, pre-rotation, and OOBI pairing
- Use cases and workflows — concrete wire payloads, search evaluation, and audit records
- System architecture — full Companion Memory data pipeline, storage topology, and forward-auth contract