Skip to content

The Lake Profile — v0.1 (draft) ​

The Lake is a thin profile on top of existing standards. It adds no new transport and no new identity system. It says which fields an A2A Agent Card must carry, how cards and messages are signed, what an offer and an agreement look like in ODRL, and which events a node publishes.

The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.

Status: draft. Breaking changes bump the major version. Nodes accept the current and previous major version.

Machine-readable schemas live in spec/schemas/. The canonical data model lives in model/.


1. Roles ​

RoleIsMust
HouseA Solid pod plus the housekeeper agent that guards itAnswer offers by its owner's rules. Never issue a grant without its owner's approval, live or by a standing rule.
VisitorAny agent asking houses for somethingCarry a signed card, state purpose, fields, retention and price.
SpecialistAn agent providing a service (ethics review, code audit, payments, record issuing)Sign what it attests.
NodeA registry and message bus anyone can runRoute and record. Hold no personal data. Make no decisions.

2. Identity: the Lake Agent Card ​

Every participant MUST publish an A2A Agent Card (A2A protocol 1.0) next to its A2A endpoint: an agent whose endpoint is https://<domain>/a2a serves its card at https://<domain>/.well-known/agent-card.json; a hosted agent at https://<host>/t/<name>/a2a serves it at https://<host>/t/<name>/.well-known/agent-card.json.

2.1 The Lake extension ​

Lake fields live inside an A2A extension so the card signature covers them. The card MUST include in capabilities.extensions:

json
{
  "uri": "https://lake.doneinfifty.com/ext/lake/v0",
  "description": "Lake profile",
  "required": true,
  "params": {
    "role": "house",
    "webId": "https://pods.example/stan/profile/card#me",
    "ownerDomain": "pods.example",
    "publicJwk": { "kty": "OKP", "crv": "Ed25519", "x": "…", "kid": "…", "alg": "EdDSA" },
    "tags": ["research-volunteers"],
    "lakeVersion": "0.1.0"
  }
}
FieldRequiredMeaning
roleyeshouse, visitor, specialist, agent or node
webIdyesThe owner's WebID: the human or organisation that answers for this agent
ownerDomainyesA domain the owner proves control of (§2.3)
publicJwkyesEd25519 public key (JWK) used for the card and every message
tagsyes, may be emptyInterest tags for discovery and guilds
lakeVersionyesSemantic version of this profile

Skills use the normal A2A skills array. Houses that take part in health studies MUST advertise a skill with id health-study.

2.2 Card signature ​

Cards MUST be signed as defined by A2A §8.4: JWS (flattened serialization) over the JCS (RFC 8785) canonical form of the card without signatures. The algorithm MUST be EdDSA (Ed25519). The JWS kid MUST equal publicJwk.kid.

A node or agent MUST reject:

  • a card with no signature,
  • a card whose signature does not verify against its own publicJwk,
  • a card whose lakeVersion major differs by more than one from its own.

2.3 Domain proof ​

A card claiming ownerDomain: example.org MUST be backed by one of:

  • a DNS TXT record at _lake.example.org containing lake-key=<publicJwk.kid>, or
  • https://example.org/.well-known/lake.json containing { "keys": ["<publicJwk.kid>"] }.

A node MUST reject a card whose domain proof fails.

Transport. Outside sandbox mode, cards, A2A endpoints and pods MUST be served over HTTPS. Solid-OIDC already requires it for pods (plain HTTP is allowed only on localhost).

Sandbox Lakes. A Lake that holds only synthetic data MAY run in sandbox mode, where domain proof is skipped and every such card is marked sandbox. A participant MUST NOT treat a sandbox-proved domain as verified outside the sandbox.

3. Messages ​

Every Lake message is an A2A message that declares the Lake extension (the A2A-Extensions header carries the extension URI; Lake cards mark it required) and whose data part carries the sender's card and a signed envelope:

json
{
  "payload": { "kind": "offer", "…": "…", "nonce": "uuid", "iat": 1792480000, "exp": 1792480300, "iss": "https://uni.example/.well-known/agent-card.json" },
  "jws": { "protected": "<base64url header>", "signature": "<base64url>" }
}
  • The signature is an Ed25519 JWS over the JCS canonical form of payload, with header { "alg": "EdDSA", "kid": "…", "typ": "lake+jws" }.
  • iss MUST resolve to a verified Lake card whose publicJwk.kid matches the JWS kid.
  • exp MUST be in the future. Receivers MUST keep seen nonces until exp and reject repeats.
  • Agents MAY message each other directly at their A2A endpoint. A node's bus is an optimisation for broadcast, never a required hop.

Message kinds in v0: offer, decision, counter, agreement, grant, result, receipt, revocation.

4. Offers and agreements (ODRL) ​

An offer is an ODRL 2.2 Offer. Its targets are canonical Lake fields. It MUST state:

TermODRL form
Purposeconstraint with leftOperand: purpose
Fieldspermission.target: canonical field IRIs
Retentionduty delete: either at lake:event/computationEnd (no retention) or within an elapsedTime
Priceduty compensate with payAmount in USD
Minimum provenance tieroptional constraint lake:provenanceTier

Offers carry the visitor's credentials (ethics approval, audit certificate) as Verifiable Credentials.

An offer for a study carries the study itself, its id in offer.study, and two attachments: an ethics approval and an audit certificate, each a Verifiable Credential together with the signed card of whoever issued it (§5.2).

An offer MAY include a free-text note. It is untrusted. A housekeeper MUST NOT let it change a decision. It may be shown to a human, quoted, and nothing else.

An agreement is the ODRL Agreement both parties sign, built from the final accepted offer, with a dateTime constraint for its end. The house signs it when it joins and returns it with the grant; the visitor MUST countersign it (a detached JWS over the agreement's canonical form) before the house runs anything. Both parties MUST keep it: the house in its pod at /agreements/. The grant names the agreement's uid and digest.

The grant issued MUST match the signed agreement: same fields, same purpose, expiry no later than the retention allows. The contract and the key cannot drift apart.

5. Decisions ​

A housekeeper answers every offer with exactly one of:

DecisionMeans
joinA standing rule the owner wrote covers this. Counts as the owner's pre-approval.
declineNo.
askThe owner decides. A request goes to their approvals device.
counterThe owner would join on different terms (fewer fields, higher price).

Decisions MUST be computed in code from the owner's rules (see rules.yaml). A model MAY write the plain-English reason shown to the owner. A model MUST NOT decide, and MUST NOT hold the power to issue a grant.

5.1 Trust ​

A credential counts only if its issuer's card verifies, its domain is proved, and that domain is on the owner's trust list:

yaml
trust:
  issuers: [hospital.example]      # who may sign records
  auditors: [audit.example]        # who may certify studies and converters
  ethics: [ethics.university.example]

An approval from a domain the owner does not list counts as no approval. Nobody appoints ethics bodies or auditors for the Lake; owners choose. Nodes MAY publish suggested trust lists, like blocklists; owners opt in.

5.2 Credentials a house checks ​

CredentialtypecredentialSubject.idIssued by
Health recordLakeHealthRecordCredentialthe owner's WebID; relatedResource.digestSRI pins the originala trusted issuer
Converter certificateLakeConverterCertificateurn:lake:converter:<package>@<version>a trusted auditor
Ethics approvalLakeEthicsApprovalthe study id; lists approved fieldsa trusted ethics body
Study certificateLakeStudyCertificatethe study ida trusted auditor

A study without a valid certificate from a trusted auditor MUST be declined, whatever the owner's rules say.

5.3 Collective counter-offers ​

Houses that counter the same offer and opted in with join_collectives: true say so in their signed counter (bloc: true). A visitor MUST treat all such counters to one offer as a single bloc whose terms are the highest price any member asks and only the fields every member accepts. Anyone holding the signed counters can check the visitor applied this rule. If the visitor accepts, it re-offers the bloc's terms to everyone who joined or countered.

(v0 note: the bloc's terms are computed from the members' signed counters rather than sent as one jointly signed counter. A jointly signed bloc counter via guilds is future work.)

6. Keys ​

  • Access is granted with Solid Access Grants: scoped to resources, time-limited (default 48 hours) and revocable.
  • Only the owner's key issues a grant: through a live Allow, or through a standing join rule the owner signed.
  • An agent MUST NOT be able to mint, widen or extend a grant. The pod's access layer MUST enforce this, not only agent code.
  • Revocation is immediate. A revoked grant MUST stop working within 1 second on the reference implementation.

6.1 The owner's key ​

The owner's signing key is made on the owner's device and never leaves it. The house holds only its public half (in /wallet/ of the owner's pod). A grant minted outside a standing rule MUST be signed by the owner's key; the house MUST check that it matches the request the owner was shown (visitor, offer, study, purpose, fields no wider, expiry no later than 48 hours) before storing or delivering it. Requests from the owner's device to the house are signed envelopes (§3).

A visitor whose offer got ask MAY send { kind: "status", offerId } to collect the outcome: the grant once the owner allows, or decline.

A grant is a LakeAccessGrant credential signed by the house, with credentialSubject: the visitor's WebID (id), offer, study, fields, purpose, authorizedBy (the standing rule, or the owner's live approval) and rules (the digest of the rules that authorised it), and a validUntil. The house stores it in /agreements/.

6.2 Money (AP2) ​

Payments follow the Agent Payments Protocol's mandate chain, as W3C VCs:

MandateSigned bySays
IntentMandatea human at the visitor's organisation (same ownerDomain, a separate key)this agent, this study, budget, intent_expiry
PaymentMandatethe visitor agentpayee (the house's WebID), amount, the digest of the Intent Mandate, the agreement uid
LakeReceiptthe payments agentpayee, amount, agreement, the Intent Mandate digest, settlement

A payments agent MUST check the whole chain and MUST refuse once the total under one Intent Mandate would exceed its budget: past that, a human must approve. Only houses that answered are paid. A house files receipts from payments agents on its owner's trust.payments list in /receipts/. In the reference implementation settlement is simulated; the mandates and receipts are real signed objects.

7. Data never moves ​

In v0 a study is a declaration, not code: up to four yes/no questions about canonical fields (age_at_least, has_code, present). Its id is urn:lake:study: + the digest of its canonical JSON.

  • The visitor sends { kind: "compute", grant, study }. The house checks the grant is its own, unexpired, for this visitor and this study, re-verifies its record chain, and reads only the granted fields.
  • The house answers with one randomised fragment: the one-hot cell its owner falls in, perturbed by unary randomised response with the study's privacy budget ε (at most 4). Any single fragment is deniable; the visitor de-biases the sum across houses.
  • Every computation is written to the house's /audit/ log (fieldsRead, claimsRead, recordsOut) and reported as a compute.done event. A node's Records moved counter is the sum of recordsOut and MUST be published, even when it is not zero.
  • Arbitrary study code needs attested execution; it is a stretch goal, not v0.

8. Events ​

Nodes publish an event stream (WebSocket, JSON). Events carry ids, kinds and counts, never record contents.

Participants report events to a node as a signed batch ({ kind: "events", events: [...] }, §3). A node MUST accept reports only from registered participants, MUST set each event's actor to the reporter's own id, and SHOULD truncate free-text values. A node registering a card MUST fetch the card from the endpoint it names and refuse it unless the same signed card is served there.

json
{ "id": "lx3k9-1f", "kind": "grant.issued", "at": "2026-10-20T12:00:00Z", "actor": "house:stan", "subject": "agent:uni.example", "data": { "hours": 48 } }

Kinds in v0:

house.joined · house.left · agent.registered · knock · offer · counter · decision · agreement.signed · grant.issued · grant.revoked · approval.requested · approval.answered · payment.settled · attack.blocked · agent.quarantined · guild.formed · study.result · compute.done · record.moved

9. Federation ​

  • Anyone may run a node. Nodes follow peers they choose and exchange signed cards and revocation notices.
  • Registries are search indexes, not gatekeepers. Any agent MUST be reachable through its .well-known card with every registry offline.
  • Nodes MAY publish and subscribe to blocklists and reputation signals. Each node chooses which lists it trusts.
  • A node publishes its signals at /signals, signed with its card's key: { kind: "signals", blocklist: [key ids], reputation: { domain: 0..1 } }. A house follows the nodes it chooses and MUST verify the signature.
  • Blocklists name keys, never domains: a forger claiming someone else's domain must never get that domain blocked. Reputation is kept only for verified identities.
  • Attack reports (attack.blocked) carry the attacker's key id, the domain it claimed, and whether that identity was verified. A node SHOULD quarantine a key after a few independent blocks (reference: 3).
  • Houses sharing an interest tag form a guild; a node announces it (guild.formed) when enough members exist (reference: 3).
  • Losing any node MUST NOT stop a mission that is already running.

10. Canonical data model ​

The Lake supports exactly one data model, in RDF, with SHACL shapes for validation. Other standards (FHIR, bank CSVs, wearables) are converted into it at the edge, inside the house, by signed and audited converter packages.

  • Core classes: Party, Thing, Claim, Provenance, Offer, Terms, Receipt.
  • Domain vocabularies (health, energy, rentals) add Claim subtypes inside the core.
  • A converted claim carries the hash of the original signed record, the converter's name, version and certificate, and the housekeeper's signature. Study code MUST verify all three and drop any value that fails.
  • Every field carries a provenance tier: self-reported, device-signed or issuer-signed.

See model/lake.ttl and model/shapes.ttl.

11. Open questions ​

These are real decisions still open. Argue in an issue.

  • Domain proof for people without a domain: delegated subdomains from pod providers?
  • Personhood: which credential prevents one person running 50 houses?
  • Selective disclosure: SD-JWT now, BBS+ later?
  • Payment: which rail settles real money, if any, and who carries the mandate liability?

Apache-2.0 · Synthetic data only · Nobody owns the Lake.