Summary
POST / returns the signed receipt only when the request body is the full CESR stream — event JSON plus all inline attachments (signatures, seal-source couples, datetime). Standard keripy HTTP-witness clients (keri.app.agenting.HTTPMessenger → keri.app.agenting.streamCESRRequests) don't put attachments in the body: they split them into the CESR-ATTACHMENT HTTP header and send only the event JSON in the body. The kerihost witness dispatcher reads only the body, sees no inline signatures, can't verify the event, and returns HTTP 204 No Content with an empty body instead of a signed receipt.
End-to-end effect: every Locksmith wallet (and any other standard keripy wallet using stock WitnessReceiptor) ends up with baser.wigs = 0 on inceptions it submits to keri.host. The wallet's WitnessReceiptor.receiptDo then loops indefinitely on "Waiting for witness receipts for ...", even though the Lambda fires and accepts the event.
This is the fourth in the keri.host compatibility series:
Diagnostic
Standard keripy streamCESRRequests in keri/app/agenting.py (keripy 2.0.0.dev6) splits each CESR event into the request body (the event JSON or msgpack) and the CESR-ATTACHMENT header (the attachments — signatures, seal source couples, datetime stamps, etc.). I captured a real wallet-produced request:
POST /
Content-Type: application/cesr
Content-Length: 345 ← only the JSON event
CESR-ATTACHMENT: -VAn-AABAAA...AAA1AAG2026-05-22T22c05c55d990493p00c00
CESR-DESTINATION: BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt
{"v":"KERI10JSON000159_","t":"icp","d":"EE56...","i":"EE56...",...}
The body has zero CESR attachments. All signatures + the datetime stamp are in the CESR-ATTACHMENT header value as a CESR-encoded counter group.
Repro
Same EE56 event posted two different ways:
Way 1: everything inline in body (curl --data-binary @file) → 200 + receipt
$ curl -X POST "https://witness.keri.host/" \
-H "Content-Type: application/cesr" \
--data-binary @/tmp/inception-full.cesr # 505 bytes: event + sig + datetime
HTTP/2 200
content-type: application/cesr
content-length: 281
<rct event + -CAB<witness eid><signature> attachment>
Way 2: keripy wire format (body=event-only, sigs in header) → 204 empty
$ curl -X POST "https://witness.keri.host/" \
-H "Content-Type: application/cesr" \
-H "CESR-DESTINATION: BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt" \
-H "CESR-ATTACHMENT: -VAn-AABAAA..." \
--data-binary @/tmp/event-only.json # 345 bytes: just the icp JSON
HTTP/2 204
content-length: 0
Same event SAID (EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZ), same witness, same destination. Only the wire-format split differs. The receipt only comes back for Way 1.
Locksmith's wallet always sends Way 2. The Lambda IS invoked (verified via CloudWatch), it just returns 204 because it has nothing to verify.
Reference implementation
keri.app.indirecting.HttpEnd.on_post (keripy reference HTTP-witness server) reassembles the full CESR stream from body + headers['CESR-ATTACHMENT'] before parsing. Roughly:
async def on_post(self, req, rep):
cr = httping.parseCesrHttpRequest(req=req)
# cr.payload is the JSON event; cr.attachments is the CESR-ATTACHMENT header value
full_stream = (
json.dumps(cr.payload).encode() if isinstance(cr.payload, dict)
else cr.payload
) + cr.attachments
self.exc.parser.parse(ims=bytearray(full_stream), ...)
The parseCesrHttpRequest helper in keri.app.httping does the body+header reassembly. Any standard keripy witness binary built on top of keripy will use this path implicitly.
Suggested fix
In the kerihost witness-handler Lambda's request-decoding step (the part that runs before the CESR parser), reassemble the full stream from body + CESR-ATTACHMENT header before passing to the parser:
// Pseudocode for the Rust handler
let body = event.body.unwrap_or_default();
let attachment = event.headers
.get("CESR-ATTACHMENT")
.or_else(|| event.headers.get("cesr-attachment")) // case-insensitive
.map(|v| v.as_bytes())
.unwrap_or_default();
let full_stream: Vec<u8> = [body.as_bytes(), attachment].concat();
parser.parse(&full_stream)?;
After that change, the existing receipt-on-fresh-event logic (#3's fix) will see the inline signatures it needs to verify, and standard keripy wallets will get the receipt they expect.
Test that would have caught this
def test_post_root_parses_cesr_attachment_header():
# Build an inception event using a hab
icp_serder, sigs = hab.makeOwnEvent(sn=0)
body = icp_serder.raw # just the JSON
attachment = sigs # signatures as CESR
response = client.post(
"/",
headers={
"Content-Type": "application/cesr",
"CESR-DESTINATION": witness_pre,
"CESR-ATTACHMENT": attachment.decode(),
},
data=body,
)
assert response.status_code == 200, "should return 200 + receipt for split-attachment request"
assert response.headers["content-type"] == "application/cesr"
assert len(response.content) > 0, "should include the receipt CESR in the body"
# Parse the response and confirm it's a `rct` event + -CAB attachment
This complements the existing test_post_root_returns_synchronous_witness_receipt (which appears to test the all-in-body shape only).
Environment
- Witness AID:
BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt
- Deployment:
serverless-witness stack in us-east-1
- keripy:
2.0.0.dev6
- Reference for the wire-format expectations:
keri.app.agenting.streamCESRRequests (sender) and keri.app.indirecting.HttpEnd.on_post (receiver)
- Test AID:
EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZ in joseph vault — wits=[BNbRf...], toad=1, wigs=0
Summary
POST /returns the signed receipt only when the request body is the full CESR stream — event JSON plus all inline attachments (signatures, seal-source couples, datetime). Standard keripy HTTP-witness clients (keri.app.agenting.HTTPMessenger→keri.app.agenting.streamCESRRequests) don't put attachments in the body: they split them into theCESR-ATTACHMENTHTTP header and send only the event JSON in the body. The kerihost witness dispatcher reads only the body, sees no inline signatures, can't verify the event, and returnsHTTP 204 No Contentwith an empty body instead of a signed receipt.End-to-end effect: every Locksmith wallet (and any other standard keripy wallet using stock
WitnessReceiptor) ends up withbaser.wigs = 0on inceptions it submits to keri.host. The wallet'sWitnessReceiptor.receiptDothen loops indefinitely on "Waiting for witness receipts for ...", even though the Lambda fires and accepts the event.This is the fourth in the keri.host compatibility series:
/oobi/<aid>/witnessshould serve witness-role endpoint replies ✅ landedPOST /should route to a CESR-typed dispatcher ✅ landedPOST /must return signed receipts in the response body ✅ landed (verified working when sigs are inline in body)POST /dispatcher must read attachments from theCESR-ATTACHMENTHTTP header, not just the body ← this issueDiagnostic
Standard keripy
streamCESRRequestsinkeri/app/agenting.py(keripy 2.0.0.dev6) splits each CESR event into the request body (the event JSON or msgpack) and theCESR-ATTACHMENTheader (the attachments — signatures, seal source couples, datetime stamps, etc.). I captured a real wallet-produced request:The body has zero CESR attachments. All signatures + the datetime stamp are in the
CESR-ATTACHMENTheader value as a CESR-encoded counter group.Repro
Same EE56 event posted two different ways:
Way 1: everything inline in body (curl
--data-binary @file) → 200 + receiptWay 2: keripy wire format (body=event-only, sigs in header) → 204 empty
Same event SAID (
EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZ), same witness, same destination. Only the wire-format split differs. The receipt only comes back for Way 1.Locksmith's wallet always sends Way 2. The Lambda IS invoked (verified via CloudWatch), it just returns 204 because it has nothing to verify.
Reference implementation
keri.app.indirecting.HttpEnd.on_post(keripy reference HTTP-witness server) reassembles the full CESR stream frombody + headers['CESR-ATTACHMENT']before parsing. Roughly:The
parseCesrHttpRequesthelper inkeri.app.httpingdoes the body+header reassembly. Any standard keripy witness binary built on top of keripy will use this path implicitly.Suggested fix
In the kerihost
witness-handlerLambda's request-decoding step (the part that runs before the CESR parser), reassemble the full stream from body +CESR-ATTACHMENTheader before passing to the parser:After that change, the existing receipt-on-fresh-event logic (#3's fix) will see the inline signatures it needs to verify, and standard keripy wallets will get the receipt they expect.
Test that would have caught this
This complements the existing
test_post_root_returns_synchronous_witness_receipt(which appears to test the all-in-body shape only).Environment
BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjtserverless-witnessstack in us-east-12.0.0.dev6keri.app.agenting.streamCESRRequests(sender) andkeri.app.indirecting.HttpEnd.on_post(receiver)EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZin joseph vault — wits=[BNbRf...], toad=1, wigs=0