---
name: w3ds-access-and-exposure
description: "Use when reasoning about who can see or write W3DS data — setting an ACL, sharing a record with several parties, deciding whether something is confidential, storing a file that must stay private, proving who authored a record, or answering 'is this secure'. Read before writing any ACL. The base skill describes the two ACL models correctly but stops short of what they do and do not protect."
license: Apache 2.0
---

# What actually gates access

The base skill says every eVault call needs `X-ENAME: @<ename>` and that two ACL models coexist. Both true. Neither sentence says what those things *mean*, and the gap between what the names suggest and what was measured is where data leaks.

Read this before writing an ACL, and before telling anyone a record is private.

## X-ENAME selects the dataset. It is not a credential.

`X-ENAME` does not merely satisfy an ACL check — it **selects which tenant's data the query addresses**. Everything outside the named tenant is invisible, *including envelopes whose ACL names you*.

Measured: the same URL and the same query, varying only `X-ENAME`, returned **131, 22 and 0** Tasks, and the three result sets had an **empty intersection**.

Two consequences, both load-bearing:

1. **ACL grants no cross-tenant access whatsoever, so it is not a sharing mechanism.** Naming a reader on an envelope's ACL does not make that envelope appear when the reader queries under their own eName. Sharing works one of two ways: address the holder's vault (impersonation, below), or write a record into the reader's own tenant.
2. This is why reference envelopes exist at all. There is no cross-tenant discovery, so the only thing a reader can find is what sits in their own tenant, and the reference tells them where the canonical lives.

**`X-ENAME` selects the tenant on writes too.** To place a file or record on someone else's vault it is not enough to name their vault in an argument — the write lands on the *actor's* tenant unless you address the request as them. The measured cost of getting this wrong: the envelope is then unreadable under your own eName, because a read addressed with your eName queries your tenant, which does not hold it.

## Only the eID signature is a boundary

A holder of a `DEVELOPER_API_KEY` can read any vault by naming it in `X-ENAME`, and **can create an envelope on any vault the same way**. The owner's consent appears nowhere in the request.

So:

- The vault an envelope sits on does **not** establish who wrote it.
- A record's presence on someone's vault is **not** evidence they put it there.
- `canonicalOwnerEName` in a body states whose the record *is*, not where it lives, and is a **claim**. Measured: one tenant held Tasks bearing 23 different `canonicalOwnerEName` values, while the tenant those names pointed at held none.

The one thing a platform key cannot forge is an **eID signature** — the wallet's ECDSA P-256 private key never leaves the device. Every provenance claim must rest on that signature, never on envelope location or on a body field.

### A platform vault cannot sign anything verifiable

Whatever public key is handed to the provisioner, a provisioned platform vault's `/whois` returns an **empty `keyBindingCertificates` array**. The standard verification chain (resolve → whois → certificates → registry JWKS) therefore has nothing to check a platform signature against.

**Never design a feature around "signed by the platform vault."** Platform-authored records are assertions. Only an end user's wallet signature is evidence.

This has a security corollary that is worse than a missing feature — see *empty certificates* in `auth-in-practice.md`.

## The legacy `acl` field cannot be read back

The eVault **strips `acl` from reads**. Nobody — not even the writer — can read back what a legacy ACL currently is.

The consequence is the important half: **every ACL bug in this system is invisible** until a human says "I cannot open this." The characteristic failure is therefore not an error but a silent half-share: a readable pointer to an unreadable artefact.

Rules that follow:

- An ACL must be **projected on every write**, never read-modify-written.
- Treat any ACL write as **unverified**. Verify it the only way available: have someone who should be able to read it try.
- Omitting `acl` defaults to `["*"]` — **public**. Which brings the next point.

### An empty audience must throw, not default

A validator that rejects the *total* failure usually lets the *partial* one through, and the partial one is what careless code produces. Three instances measured 2026-09-15, the third being our own:

1. An empty ACL is refused; a **shrunk** ACL — rebuilt from an incomplete read, one participant short — is not.
2. A payload with no seal is refused; one whose seal no longer covers the body (signer list or document hash moved after sealing) is not. That is *worse* than publishing nothing: a reader recomputes, finds the mismatch, and concludes the record was tampered with.
3. Client code containing `acl: input.acl?.length ? input.acl : ["*"]` does not refuse an empty ACL at all — **it publishes**. A caller that computed an audience and got an empty list because a lookup failed has just made the record world-readable, and no error is raised anywhere.

If your write helper has that line, change it to throw. An empty audience is a bug in the caller, and the only safe reading of it is "refuse."

### `_acl` is real, populated, and behaves differently on update

The newer `_acl` block is not merely in the schema. Measured 2026-09-09 across `Message`, `Chat`, `File`, `Task`, `CallSession` and `Summary`, eight envelopes each: **48 of 48 carried `_acl`**.

Shape: `_acl { v default_perms grants { ename perms } denials { enames } require { ontology path op value } }`. `require` is `[[AclCondition]]` and needs a subfield selection or the query errors.

Decision order: **denials** (deny always wins) → the single **most specific direct grant** (user > platform > group; a direct grant is final and never falls through) → the **ontology conditions** (OR of ANDs over numeric JSONPath thresholds).

Two measured facts that matter more than the bitmask:

- `default_perms` was **0** on 47 of 48 (nobody by default), and 15 on one.
- Every one of the **159** sampled grants carried `perms: 15` (`0x0F`, full access). Nobody is using the granular READ/CREATE/UPDATE/DELETE mask yet, so in practice access is still all-or-nothing per eName. Do not promise a user fine-grained permissions on the strength of the schema.

And the divergence that makes the two models un-unifiable: on **update**, legacy `acl` is an input to the mutation, so omitting it does not carry the previous audience forward; the newer `_acl` block is *left alone* by an update that does not carry it. Same call, opposite consequences. Never reason about the two as one thing.

## ACL does not gate listing

Measured: ACL does not control whether an envelope appears in a `metaEnvelopes` listing. Envelopes whose ACL names a reader still do not appear under that reader's `X-ENAME` — the tenant selection decided that first.

What ACL governs *beyond* storage is **not established**. Treat it as a record of who was intended to have access — evidence about audience — and not as an enforcement boundary you can rely on.

## File bytes are public. ACL never touches them.

This is the finding most likely to turn into an incident.

`uploadFile` returns a `publicUrl` on a **public-read CDN** and defaults `acl` to `["*"]`. The bytes are reachable over plain HTTPS with **no Authorization, no X-ENAME, no cookie**. The only thing between a stranger and the content is knowledge of the URL — and the URL sits in the File envelope, which any holder of a developer key can read by naming the owner in `X-ENAME`.

**Confidentiality of file content must come from encrypting before upload.** It is not a property the platform provides.

And the bytes outlive the record. Measured 2026-09-18: after `deleteMetaEnvelope`
returned `true` for a blob envelope, the CDN kept serving the same `publicUrl`
with **HTTP 200**. Deleting the envelope removes the *record*, not the object —
so an upload is effectively permanent and world-readable from the moment it
happens, and "we deleted it" is not a remediation. Treat every `uploadFile` as
publication.

And note what cannot be built today to fix it: the eID wallet's crypto surface is `sign()` and `getPublicKey()` only. There is **no decryption primitive**, so end-to-end encryption between W3DS participants is not buildable with wallet keys as they stand. (`w3ds://reveal` looks like a counter-example; it performs no cryptography at all.)

## An attachment is its own envelope

A message and its attachment are **two envelopes with two independent ACLs**. Granting access to the message grants nothing about the file.

Any widening, sharing or forwarding must walk `fileId`, `mediaUrl`, `thumbUri` and `attachments[]` and widen **each**. Otherwise the reader gets a message they can open, pointing at a file they cannot — the silent half-share again.

## Share by ACL, not by copying

When several parties must see an event, name them on the ACL of the **one authoritative record** rather than writing a copy into each of their vaults.

Two things this buys. It removes a forgery surface: a copy in your vault is *the platform's word about someone else*, while the original is *the actor's word*, checkable against their signature. And it is what the protocol is for — one owner, one home, many readers.

The write-anywhere power that lets a platform put a record in somebody's vault is exactly the power that makes such a copy worthless as evidence.

## Checklist before calling a record protected

- [ ] The ACL was projected on the write, not read-modify-written, and is treated as unverified until a real reader confirms.
- [ ] An empty computed audience throws instead of defaulting to `["*"]`.
- [ ] Which ACL model the record uses is stated, and update semantics for that model were checked.
- [ ] No provenance claim rests on envelope location or on a body field — only on a wallet signature.
- [ ] Any file whose content is confidential was encrypted **before** upload.
- [ ] Every attachment reachable from a shared record had its own ACL widened.
- [ ] Nobody was told the record is invisible to others on the strength of its ACL alone.
