Skip to content

Add scoped execution receipt helpers for clas.execution.receipt.v1#55

Merged
GsCommand merged 5 commits into
mainfrom
codex/add-sdk-support-for-execution-receipts
Jun 13, 2026
Merged

Add scoped execution receipt helpers for clas.execution.receipt.v1#55
GsCommand merged 5 commits into
mainfrom
codex/add-sdk-support-for-execution-receipts

Conversation

@GsCommand

Copy link
Copy Markdown
Contributor

Motivation

  • Add SDK support for the new clas.execution.receipt.v1 shape that uses scoped proofs[] so agents can sign execution while settlement is attested separately.
  • Provide helpers to construct, sign, attach, and verify scoped proofs while enforcing rules that prevent execution proofs from covering settlement and settlement proofs from covering action.
  • Preserve existing receipt/trust behavior and avoid publishing raw payment tx hashes or stealth addresses per the privacy/security design rules.

Description

  • Added src/execution-receipt.ts which defines types and constants (EXECUTION_RECEIPT_SCHEMA, EXECUTION_PROOF_COVERS, SETTLEMENT_PROOF_COVERS, ClasExecutionReceiptV1, ClasScopedProof, etc.) and implements createExecutionReceipt, signExecutionProof, attachSettlementProof, signSettlementProof, verifyExecutionReceipt, and assertSafeReceipt using json.sorted_keys.v1, SHA-256, and Ed25519.
  • Enforced exact cover semantics where execution proofs cover exactly `[

Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c24c05baea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/execution-receipt.ts Outdated
if (!signatureValid) proofErrors.push("Proof signature is invalid");
proofs.push({ type: proof.type, signer: proof.signer, covered_fields: proof.covers, hash_matches: hashMatches, signature_valid: signatureValid, ok: proofErrors.length === 0, errors: proofErrors });
}
return { ok: errors.length === 0 && proofs.length > 0 && proofs.every((p) => p.ok), proofs, errors };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require an execution proof before returning ok

For a receipt that contains only a valid settlement proof, ok becomes true because this check only requires at least one proof and all present proofs to pass; assertSafeReceipt also only requires a settlement proof when settlement exists. In that scenario the payer/rail key can make arbitrary verb/agent/action fields look verified even though no execution proof covers them. Require at least one valid execution proof before returning success.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts Outdated
if (!hashMatches) proofErrors.push("Proof hash does not match covered payload");
const publicKey = await resolveKey(proof, options);
if (!publicKey) proofErrors.push(`No public key available for kid ${proof.signature.kid}`);
else signatureValid = await subtle.verify("Ed25519", await importPublicKey(publicKey), fromB64(proof.signature.value), new TextEncoder().encode(canonical));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Catch key parsing failures during verification

When a caller or key resolver supplies a malformed PEM/DER value, importPublicKey(publicKey) throws and verifyExecutionReceipt rejects instead of returning { ok: false, errors: ... } like the rest of the verifier path. This is especially visible for malformed/untrusted receipts where structural errors have already been collected but this line still tries crypto verification; wrap key import/verify failures and append them to proofErrors.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts Outdated
for (const proof of receipt.proofs) assertProofCovers(proof);
if (receipt.settlement) {
if (receipt.settlement.stealth_address !== undefined) throw new Error("settlement.stealth_address must not be published");
if (typeof receipt.settlement.payment_ref === "string" && /^0x[a-fA-F0-9]{64}$/.test(receipt.settlement.payment_ref)) throw new Error("settlement.payment_ref must not be a raw 0x transaction hash");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject raw tx hashes outside payment_ref

When callers include an allowed extra settlement field such as { tx_hash: "0x...64 hex chars..." }, createExecutionReceipt and verifyExecutionReceipt accept and publish the raw transaction hash because this privacy check only inspects payment_ref. Since ClasExecutionSettlement explicitly allows arbitrary settlement fields, scan/reject raw hash-looking values in those fields or disallow transaction-hash aliases as well.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts Outdated
if (!hashMatches) proofErrors.push("Proof hash does not match covered payload");
const publicKey = await resolveKey(proof, options);
if (!publicKey) proofErrors.push(`No public key available for kid ${proof.signature.kid}`);
else signatureValid = await subtle.verify("Ed25519", await importPublicKey(publicKey), fromB64(proof.signature.value), new TextEncoder().encode(canonical));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate proof algorithm metadata

For receipts parsed from JSON, an intermediary can change proof.canonicalization or proof.signature.alg to an unsupported value and this verifier still returns the proof as ok because it always canonicalizes with sorted keys and verifies with Ed25519 without checking those fields. Reject proofs whose advertised canonicalization/signature algorithm is not the expected value before reporting them as valid.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts
}
export async function verifyExecutionReceipt(receipt: ClasExecutionReceiptV1, options: VerifyExecutionReceiptOptions = {}): Promise<VerifyScopedExecutionReceiptResult> {
const errors: string[] = [];
try { assertSafeReceipt(receipt, { requireSettlementProof: options.requireSettlementProof }); } catch (err) { errors.push(err instanceof Error ? err.message : String(err)); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the receipt schema before returning ok

When verifying external JSON, this path never checks that clas is "1.0" or that schema is clas.execution.receipt.v1; a valid signed action can be copied into a receipt whose top-level schema/version was changed and still get ok: true. Since this helper is specifically accepting the new execution receipt shape, reject mismatched top-level identifiers before proof validation succeeds.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts
return Object.fromEntries(covers.map((field) => [field, (receipt as unknown as Record<string, JsonValue | undefined>)[field]]));
}
async function createProof(receipt: ClasExecutionReceiptV1, covers: readonly string[], type: ClasProofType, options: SignScopedProofOptions): Promise<ClasScopedProof> {
const canonical = canonicalize(coveredPayload(receipt, covers) as JsonValue);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind the proof signer into the signed payload

The signature is computed only over the fields named by covers, so proof.signer can be edited after signing and verifyExecutionReceipt still reports ok: true with the forged signer. This makes the returned signer untrustworthy whenever callers verify with publicKeyPem/publicKeysByKid; include the signer in what is signed or otherwise validate that the proof metadata is bound to the key.

Useful? React with 👍 / 👎.

Comment thread src/execution-receipt.ts
}
export function assertSafeReceipt(receipt: ClasExecutionReceiptV1, options: { requireSettlementProof?: boolean } = {}): void {
for (const proof of receipt.proofs) assertProofCovers(proof);
if (receipt.settlement) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject settlement proofs when settlement is absent

If a receipt contains a settlement proof but no receipt.settlement, this safety check skips settlement validation entirely; a signer can then create a valid settlement proof over just receipt_id because the missing field is omitted during canonicalization, and the verifier reports that settlement proof as ok. Reject settlement proofs unless the receipt actually contains the settlement data they are supposed to attest.

Useful? React with 👍 / 👎.

@GsCommand
GsCommand merged commit 239b0a2 into main Jun 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant