Skip to content

Add opt-in client-side payload encryption hooks across SDKs #217

Open

Description

@rmcdaniel

User need and decision

Applications need selected business payloads to remain unreadable to the Server, database and their operators. Accept an opt-in client/worker payload-protection extension across embedded PHP and the PHP/Python/Rust service SDKs. durable-workflow/durable-workflow.github.io#306's application-owned encryption recipe is an interim explanation, not completion of this product requirement.

The PHP SDK currently requires AvroPayloadCodec. Preserve the official Avro value protocol. A protection transform is not permission to add a JSON fallback, arbitrary replacement wire codecs or a new codec-server service.

Acceptance

  • Define a supported opt-in encode/decode hook and versioned authenticated ciphertext representation, including nonsecret key/version metadata. Specify the protected boundaries for workflow/activity inputs and results, signals, updates and queries. Unsupported boundaries must be explicit.
  • Keep plaintext outside the Server when protection is enabled. Keys belong to caller/worker configuration, not Server settings, logs, histories or telemetry. Use established cryptographic libraries; do not implement cryptography.
  • Preserve replay: encryption nonces/randomness are generated only at a new admission/commit boundary, never regenerated as a workflow side effect during replay. Canonical histories and duplicate/retry intent must remain stable. Coordinate request deduplication with Make signal and update retries safe after a lost response #216.
  • Specify key rotation and retained-history compatibility. Old keys remain usable for required history; missing/wrong keys, tampered bytes and unknown versions fail explicitly without falling back to plaintext.
  • Qualify exact typed int64/null/Unicode/maps and inline/external values with independently installed PHP/Python/Rust SDKs, real published Server admissions, cold-process replay and worker replacement. Include typed failures and payload-size accounting without weakening original deadlines or authority fences.
  • Describe the plaintext metadata boundary: IDs, type/command names, namespaces/queues, searchable fields, memo/labels and error text are not automatically protected. Define deliberate redaction for operator surfaces. Waterline/CLI must display protected/unavailable data honestly rather than a corrupt or empty value.
  • Correct the current website claims immediately: there is no released pluggable payload encryption hook or codec-server architecture. Final guides include a runnable supported hook and key-rotation example in every current locale. No compliance or data-residency claim follows from encryption alone.

Priority P2, additive product feature. No implementation is shipped. General owns the public SDK/Workflow implementation; native capability/catalog adoption requires separate durable-workflow/server#325 coordination.

Activity

  1. added
    kind:featureA public product capability or experience is requested
    kind:cross-repositoryWork spans more than one public repository
    priority:P2Normal-priority product work
    status:readyReady for implementation
    on Oct 20, 2026
  2. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Additional need from source review (Slack miner case 2d1d1124): teams using KMS envelope encryption hit KMS rate limits when every decode during replay unwraps a data key. Two points for the hook's acceptance, if they aren't already covered:

    • Key provider: a key-provider interface that supports KMS-wrapped data keys with a bounded in-process data-key cache, so replays don't call KMS for every payload.
    • Per-key usage limits: with random nonces, AES-GCM has a safe-use bound per key. High throughput should rotate data keys before reaching it.
  3. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Ownership

    A dedicated Claude CLI maintainer session now owns this issue through implementation across embedded PHP and the PHP/Python/Rust service SDKs, cross-language interoperability tests, qualification against the published Server, compatible publication and documentation in every current locale. Its assigned delivery order is shared #216 first, then workflow #871, then this issue.

    Work lands on claude/217-* branches with early draft pull requests. The shared envelope contract (versioning, authenticated metadata, key rotation, replay of retained history, external payloads, duplicate/intent comparison with #216, KMS data-key caching and per-key usage bounds, failure handling) will be recorded here for review before the first implementation PR leaves draft. Encryption stays opt-in and existing unencrypted behavior is preserved. Native Server adoption remains coordinated through server#325.

  4. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    The operator transferred this assignment to General after the prior Claude checkpoint. Delivery order remains shared #216, Workflow #871, then this opt-in encryption feature. The source investigation and KMS/cache/usage-limit requirements are recovered. I retain full implementation, contract review, all-locale documentation and published/installed acceptance; no encryption capability or plaintext-protection claim follows from this handoff.

  5. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Recovered source investigation

    The prior source investigation is recovered. Current encryption is still unavailable; these findings do not advertise a codec or authorize production-key access:

    • Core and all service SDKs use the published Avro Value envelope. Existing decode paths enforce its framing/fingerprint and the envelope's exact fields; opaque ciphertext cannot simply replace Avro bytes.
    • Server decodes and validates several signal/update/query paths and re-encodes some values. Keeping plaintext outside Server therefore requires deliberate pass-through/validation behavior, rather than only adding a client helper.
    • New-command encoding, replay comparisons, retained receipt intent, service-operation identity and local/prepared activity checkpoints must remain stable across retries and replays. Random encryption nonces must not turn identical logical commands into conflicts. Shared Make signal and update retries safe after a lost response #216's future protected-intent boundary is not yet implemented or qualified.
    • Encryption must precede external-payload thresholding and hashing so limits, transport and retention operate on the actual retained bytes. Tests must include external payloads and cross-SDK cold replay.
    • Memo/search attributes, identifiers, routing names, command attribution, failures and logs have separate privacy surfaces. Specify what is protected and what remains readable; operator views must show an explicit protected/unavailable state, never fabricated empty data or decoded secrets.
    • Use established authenticated-encryption libraries, versioned portable envelopes, key rotation and explicit wrong-key/tamper refusals. The accepted KMS work includes bounded data-key caching and per-key usage limits. Default behavior remains unchanged and opt-in requires a fully qualified compatible path.

    After #216 and Workflow #871, I will finalize the portable threat/compatibility contract before implementation and verify PHP/Python/Rust/Core, external transport, operator surfaces, key rotation/limits and all eight documentation locales on published artifacts. No new dependency, wire, schema, Cloud/provider or frozen-actor adoption follows from this recovered investigation.

  6. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    General open-item pass, November 4. Owner: General for threat/compatibility design; SDK/Core owners for hooks; docs for truthful claims.

    Next action: Finish the portable envelope/key-rotation/replay and216 intent interaction design before opt-in hooks; correct any currently implied shipped encryption capability now.

    Action taken in this pass: Assigned docs the source-only truth check and it found the eight-locale workflow-ID wording to correct. No crypto implementation, provider/key provisioning or frozen wire adoption is admitted.

    General retains the cross-repository follow-through. Existing operator stops and exact source/release/production gates remain binding; the item stays open until its actual acceptance is evidenced.

  7. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    The requested source-only website truth check found one remaining implication in the Workflow ID guide: it said payload codecs included payload encryption without stating that no supported hook is released. Docs draft #471 corrects that paragraph in all eight locales at exact 34df822b462a12967eca38f9e288ed3f04c4bd58, base 563f81448607ed1562499d876150fa1566a78702, tree a9cad92e7c1d595603608f5a78fe1193e4ca43d8.

    The guide now says Avro encoding does not encrypt payloads or protect identifiers, released packages have no supported client-side encryption hook, and this issue tracks that feature. All existing plaintext metadata, opaque-reference, application data-handling and authorization warnings remain, as do all executable examples. The source audit also checked the capability index and deployment guide: their Avro-interoperability, plaintext-ID, TLS and storage-owned at-rest statements do not claim shipped encryption hooks.

    Named owner durable-workflow-github-io retains this immediate correction, with General retaining merge after normal hosted checks and independent Security review. Local translation/contract checks pass; hosted eight-locale qualification is running. Protected Pages and all eight live privacy paragraphs remain post-merge acceptance. General retains the feature contract/implementation and its #216 / Workflow #871 order. No payload protection, codec-server architecture, encryption library, key access or release is introduced or claimed by this docs slice.

  8. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    The immediate current-site truth correction requested by this issue is complete. Docs #471 merged normally as 2706af78b1950539a9918ff05a1f62cb54026dab after 11 successful normal checks and independent Security PASS. The sole automatic protected Pages run 41915687309, deployment 7558363450, and all six exact-main workflows succeeded.

    All eight live Workflow ID privacy paragraphs match accepted localized source and link this existing feature issue. They explicitly state that Avro does not encrypt payloads or protect identifiers and released packages have no supported client-side payload encryption hook. Plaintext metadata, opaque-reference, application data-handling and authorization warnings remain. All seven reviewed-source catalogs match the canonical English hash, and the entire live stable manifest is unchanged. The merged PR branch was deleted and exact absence verified.

    Complete owning live receipt. This closes only the immediate misleading-public-wording slice. The opt-in encryption feature remains unshipped and open under General's #216 / Workflow #871 / #217 sequence, with no codec-server, key access, package/runtime or header-rollout claim.

  9. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Workflow: not started. The embedded encryption hook comes after workflow#871 and the threat/compatibility contract (General). Owner: workflow.

  10. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Waterline (2026-11-11): no Waterline slice assigned. If the envelope contract requires operator views to show protected payloads as unavailable, Waterline will take that as a follow-up issue then.

  11. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Server: nothing needed yet; payloads stay opaque to the Server. I'll open a native capability issue if the frozen envelope needs one. Owner: General (design).

  12. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    PHP: not started. Waits on the envelope and key-rotation contract. Owner: General, then sdk-php.

  13. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Design decision (General, 2026-11-13): payload protection envelope v1. This is the contract to build to; implementation PRs refine wording, not semantics.

    Where it sits. Protection is a transform that runs after the official Avro value encoding on write and before Avro decoding on read. Avro stays the only wire codec: there's no JSON fallback, no replacement codec and no codec server. A protected payload is an Avro value with a reserved envelope type.

    Envelope (v1): { "dw_protected": 1, "alg": "xchacha20poly1305-ietf", "key_id": "<caller-chosen, nonsecret>", "nonce": <24 bytes>, "ciphertext": <bytes> }

    • Algorithm: XChaCha20-Poly1305 (IETF AEAD) from each language's established library: libsodium/sodium_* in PHP, cryptography or PyNaCl in Python, RustCrypto chacha20poly1305. No hand-rolled crypto. The 192-bit random nonce makes random nonces safe.
    • AAD = dw_protected version, alg, key_id, namespace and the boundary kind (e.g. workflow_input, activity_result, signal_args). Ciphertext can't be moved to another namespace or to another kind of payload.
    • Nonce: generated only at the moment a payload is first encoded for admission or commit. That covers a client start, signal, update or query, an activity completion, and a workflow-task completion carrying new commands. Replay decodes history and never re-encrypts it. Retried requests resend the same bytes, so dedupe (Make signal and update retries safe after a lost response #216) stays stable.

    Protected boundaries (v1): workflow input and result; activity input and result; signal args; update args and result; query args and result; heartbeat details; the details payload of failures (not the message text); child and continue-as-new inputs.
    Not protected, and documented as such: IDs, workflow/activity/signal/update names, namespaces, queues, search attributes, memos, labels and failure message text. Applications must keep secrets out of those.

    Keys. Keys live only in the client or worker configuration as a keyring: one active key_id for encrypting, plus every retained key for decrypting. They never go in Server settings, histories, logs or telemetry. Rotation means adding a new active key while keeping the old ones for as long as history that uses them is retained.
    Failures are typed and never fall back to plaintext:

    • payload_key_unavailable: an unknown key_id;
    • payload_authentication_failed: tampered bytes, or the wrong key or AAD;
    • payload_envelope_unsupported: an unknown version or alg.
      A worker that hits one of these fails the task and doesn't continue on garbage.

    Server. No change. Payloads stay opaque, and size limits count the envelope bytes.
    Operator surfaces. Waterline and the CLI show protected (key <key_id>) instead of a value, never an empty or corrupt one. Decoding there is out of scope for v1.

    Owners and order:

    • sdk-python: reference implementation plus a cross-SDK conformance vector set: typed int64, null, Unicode, maps, inline and external values, tamper, wrong key, unknown version.
    • sdk-php: implementation passing the same vectors.
    • sdk-rust: implementation passing the same vectors.
    • workflow (embedded): the same transform at embedded boundaries, with a replay-regression fixture.
    • waterline and cli: protected display.
    • docs: replace the interim recipe (Bind the planned manual PHP qualification workflow in protected policy #306) with the supported feature, and state the plaintext metadata boundary.

    The qualification bar in the acceptance above still applies: independently installed SDKs, a published Server, cold replay and worker replacement.

  14. rmcdaniel commented on Nov 14, 2026

    @rmcdaniel
    MemberAuthor

    Security contract review — current SDK/embedded boundaries, exact AAD, and vectors

    Disposition: HOLD implementation until the items below are settled in the v1 contract. I reviewed the accepted decision against current main at sdk-python 7a36e433, sdk-php 92053fc1, sdk-rust 1129d797, and workflow 8bb05a63. This is a contract/source review only; I used synthetic keys and no Server, provider, customer data, or live runtime.

    1. Envelope recognition must be a distinct Avro writer type

    The current Value schema has only null/scalar/array/MapValue branches, and Python converts every ordinary dictionary to MapValue (schema, map conversion). PHP and Rust have the same fixed fingerprint. A map with keys dw_protected, alg, key_id, nonce, and ciphertext is therefore ordinary user data today. Key recognition would collide with valid application maps.

    Use a separate, top-level Avro single-object writer schema, not a magic map and not a recursive branch in Value:

    {"type":"record","name":"ProtectedValue","namespace":"durable_workflow.protocol","fields":[{"name":"dw_protected","type":"int"},{"name":"alg","type":"string"},{"name":"key_id","type":"string"},{"name":"nonce","type":"bytes"},{"name":"ciphertext","type":"bytes"}]}

    Its parsing canonical form has CRC-64-AVRO fingerprint 0ba70c3fd6c311ca. The existing plain Value fingerprint remains e2a33dff55802237. Dispatch by the single-object fingerprint. Ordinary marker-shaped maps continue to decode as MapValue; only ProtectedValue enters the decrypt path. Reject dw_protected != 1, extra/invalid structure, a nonce other than 24 bytes, or ciphertext shorter than the 16-byte tag before key lookup. This also avoids nested protected branches.

    The read pipeline wording must be precise: base64 + single-object framing -> fingerprint dispatch -> decode the outer ProtectedValue record -> validate/decrypt -> decode the authenticated inner bytes with the retained official Value writer schema. Protection is before application Value decode, but recognition necessarily decodes the outer Avro record. Current readers reject every unknown fingerprint (Python); v1 must retain both exact schemas.

    2. Canonical AAD bytes

    The field list in the decision is not enough for cross-SDK authentication. I propose this exact v1 byte string:

    AAD = b"durable-workflow.payload-protection\x00"
        || u8(1)
        || lp(alg) || lp(key_id) || lp(namespace) || lp(boundary_kind)
    
    lp(s) = u32be(len(UTF8(s))) || UTF8(s)
    

    All four strings must be valid UTF-8; no Unicode normalization or locale conversion. alg is exactly ASCII xchacha20poly1305-ietf. key_id should be restricted to nonempty nonsecret ASCII [A-Za-z0-9._:-]{1,128}. boundary_kind is a closed ASCII enum. Namespace is the exact namespace whose worker will decrypt the payload; cross-namespace calls must bind the target/callee namespace for inputs and the receiving/caller namespace for returned values.

    Use destination semantics consistently. In particular, direct, scheduled, child, and continue-as-new starts all arrive as workflow input. Either bind all four to workflow_input, or preserve an authoritative origin discriminator through Server/task/history and use the same discriminator on decrypt. Encrypting as child_workflow_input and later decrypting generically as workflow_input cannot authenticate.

    Required substitution controls: same bytes under a different namespace; every other boundary enum; boundary case change; empty namespace; malformed UTF-8; reordered/omitted AAD field. All must refuse before inner Avro decode.

    3. Actual payload-boundary inventory is incomplete

    The current SDK has more durable payload surfaces than the accepted list: workflow side-effect results (Python), workflow/message stream items, scheduled workflow input, Nexus request/result, local/prepared activity reports, and external task results. These can carry the same business secrets as the named boundaries. V1 must either protect each with an exact boundary kind or list it explicitly as plaintext/unsupported and refuse protection configuration at that call site; it must not imply broad payload protection.

    Heartbeat details are not an Avro payload today: Python sends the raw details object (current wire) and embedded storage keeps it as an array. The decision cannot claim protected heartbeat details without a coordinated envelope/storage migration. Failure details are likewise not uniform: activity failure details have an envelope path, while workflow/update/query failure structures contain plaintext diagnostics and do not all expose a details envelope.

    Before implementation, publish one table per current call site: producer, stored/wire field, decrypting consumer, namespace source, boundary kind, inline/external support, and v1 protected/explicitly-plaintext status. Memos, search attributes, labels, names, queues, IDs, and failure message/stack text remain deliberately plaintext as already decided.

    4. Retry, replay, and external storage rules

    Generate the random nonce and protected blob exactly once after replay has selected a new admission/commit payload. Do not put randomness in generic replay-time Value serialization. Current Python forms request_json before its transport retry closure (request path) and serializes workflow commands once before completion retries (worker path); preserve those properties in every SDK.

    Automatic retries must reuse the exact protected blob and request body. A manual/crash-recovery retry that reconstructs the same plaintext and request_id will produce a new random nonce/ciphertext and can conflict with #216's retained intent hash. The contract must choose one safe behavior: expose a persistable prepared protected request/blob that callers can resend, or explicitly scope byte-stable retry to the original call/process and remove any broader same-plaintext retry promise. Do not derive a nonce only from request_id; reusing a request ID with changed plaintext would reuse the nonce under one key.

    Protect first, then threshold/offload the resulting outer Avro blob. Current external storage stores the base64 Avro blob bytes (Python); storing before protection would leak plaintext. On read: fetch and verify the external size/hash, then outer fingerprint/decrypt/inner decode. Retries must reuse the same uploaded object/reference. Define how Waterline/CLI learn key_id for an external reference: either an integrity-checked fetch or explicit nonsecret reference metadata. Until then they can honestly show protected (external key unavailable), not invent a key or show an empty/corrupt value.

    5. Rotation, downgrade, and typed refusal

    Each keyring entry is exactly 32 bytes. One active key_id writes; lookup by the envelope's exact ID reads. Reject duplicate IDs/config shadowing, and state that an ID must never be reassigned to different key bytes. Old IDs remain until every required history/external object has expired. Never try every key and never fall back from a recognized protected envelope to plain decode.

    The current three errors need two more decisions:

    • A plain Value at a boundary configured to require protection is a downgrade. Add payload_protection_required, or define an equally typed strict policy. An explicit migration mode may accept old plaintext histories, but must be documented as lacking downgrade protection; it cannot be an automatic fallback.
    • Authenticated plaintext that is not one complete retained Avro Value frame needs a stable typed error such as payload_plaintext_invalid; it is neither an unknown key nor an authentication failure.

    Error precedence should be: fingerprint/outer structure/version/alg -> payload_envelope_unsupported; known v1 + unknown ID -> payload_key_unavailable; known ID + AEAD failure (ciphertext/key/AAD) -> payload_authentication_failed; successful AEAD + malformed inner frame -> the chosen plaintext-invalid error. Messages/telemetry must not include key bytes, plaintext, nonce, or ciphertext.

    For Python, the current cryptography AEAD API exposes 12-byte ChaCha20-Poly1305, not XChaCha20-Poly1305. The Python implementation should require PyNaCl's libsodium crypto_aead_xchacha20poly1305_ietf_* binding (or a future library API that explicitly implements XChaCha); cryptography or PyNaCl is not currently an interchangeable choice.

    6. Minimal deterministic synthetic vector

    Fixed key/nonce are test-only; production uses the OS CSPRNG. This single positive covers signed int64, null, Unicode, a nested map, inline transport, and the exact bytes later used as an external object.

    value (keys in this order) = {
      "i": -9223372036854775808,
      "m": {"k": "v"},
      "n": null,
      "u": "Iñtërnâtiônàlizætiøn ☃ 𐍈"
    }
    namespace = "tenant-a"
    boundary  = "workflow_input"
    key_id    = "vector-key-1"
    key       = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
    nonce     = 000102030405060708090a0b0c0d0e0f1011121314151617
    AAD       = 64757261626c652d776f726b666c6f772e7061796c6f61642d70726f74656374696f6e000100000016786368616368613230706f6c79313330352d696574660000000c766563746f722d6b65792d310000000874656e616e742d610000000e776f726b666c6f775f696e707574
    inner     = c301e2a33dff558022370e08026904ffffffffffffffffff01026d0e02026b0a027600026e0002750a4849c3b174c3ab726ec3a27469c3b46ec3a06c697ac3a67469c3b86e20e2988320f0908d8800
    ciphertext+tag = 5dc3eddcad2dd82e117328c6c93bac17b4b8d75a0229e2147e1852b83faf9e535c70b6e62f670b11dfdde79a7e5590c903d421fb331d38ad69583351e69d8752c6493c0b66b9861daf7fd651d6ed097dd23b12f1a5be86cc6dd24eecb9163a
    outer base64 = wwELpww/1sMRygIseGNoYWNoYTIwcG9seTEzMDUtaWV0Zhh2ZWN0b3Ita2V5LTEwAAECAwQFBgcICQoLDA0ODxAREhMUFRYXvgFdw+3crS3YLhFzKMbJO6wXtLjXWgIp4hR+GFK4P6+eU1xwtuYvZwsR393nmn5VkMkD1CH7Mx04rWlYM1HmnYdSxkk8C2a5hh2vf9ZR1u0JfdI7EvGlvobMbdJO7LkWOg==
    external object = the 228 ASCII bytes of outer base64
    external sha256 = 2647b8aa410b36f1de317bf0946052967585f79d7ea025e6fc9f2efc3967c6d3
    

    Required negatives derived from that one vector:

    Mutation Expected result
    ordinary Value/MapValue with the five marker-shaped keys decodes as the original user map, never as an envelope
    outer version 2 or other alg payload_envelope_unsupported before key lookup
    unknown key_id payload_key_unavailable
    flip any nonce/ciphertext/tag byte, or use a wrong known key payload_authentication_failed
    namespace tenant-b payload_authentication_failed
    any other boundary kind payload_authentication_failed
    valid AEAD whose plaintext is truncated/unknown-fingerprint/trailing-byte Avro plaintext-invalid typed refusal
    plain Value under strict protection policy downgrade/protection-required typed refusal
    same inline protected blob externalized identical decrypted value; size/hash/reference verified before decrypt

    I generated and round-tripped the vector with fastavro 1.12.1 and PyNaCl 1.6.0/libsodium. General needs to settle the separate top-level schema, exact AAD, strict-vs-migration read policy, crash-retry contract, and the actual boundary table before implementation starts. Once settled, these bytes can be checked into the cross-SDK vector corpus unchanged.

  15. rmcdaniel commented on Nov 14, 2026

    @rmcdaniel
    MemberAuthor

    Contract decisions following the concrete Security review 6876199565:

    1. Accept the distinct top-level durable_workflow.protocol.ProtectedValue Avro writer schema and fingerprint 0ba70c3fd6c311ca from that review. Retain the official plain Value schema/fingerprint unchanged. Recognition is by the framed writer fingerprint; marker-shaped user maps remain ordinary values. Decode only the outer record before authentication, then decode one complete authenticated official Value frame.
    2. Accept the review's exact domain-separated, versioned, length-prefixed AAD bytes and destination namespace semantics, with its closed key-ID format. Direct/scheduled/child/continue-as-new inputs all use workflow_input. Retain the supplied deterministic synthetic vector unchanged and its substitution/malformed-frame controls. Use established XChaCha20-Poly1305 libraries, including PyNaCl/libsodium for Python; do not implement cryptography.
    3. Protection remains opt-in. Once enabled for a boundary, strict reads are the default: plain Value is payload_protection_required. Explicit migration mode may read old plaintext at named boundaries and must state that it lacks downgrade protection. A recognized protected envelope never falls back to plaintext in either mode. Accept the review's error precedence and add payload_plaintext_invalid after successful AEAD with an invalid inner frame.
    4. Automatic transport/completion retries resend the original protected bytes, body, caller command key, and uploaded reference. Cross-process retry needs a persistable prepared request containing the original operation, destination/namespace/boundary, command key, and protected payload/reference, without authentication credentials or key material. Resending that prepared request preserves Make signal and update retries safe after a lost response #216 intent; reconstructing plaintext with the same key is not promised to do so and may correctly conflict. Nonces are random only for new protected payload admission; never derive them from a request key or regenerate them during replay. Protection precedes size accounting/offload; external hash/size verification precedes decrypt.
    5. Required protected v1 targets are workflow input/result, activity input/result, signal input, update input/result and query input/result. Include side-effect results at their durable commit boundary. All start origins share workflow input; ordinary/local/prepared/external activity paths must either use the same supported activity boundary or explicitly refuse protection at an unsupported path. Do not claim support from an unwired codec alone.
    6. Heartbeat details currently use raw JSON and require a separate coordinated envelope/storage change; they are explicitly unsupported in the initial contract, not silently protected. Streams and Nexus also require exact call-site mappings before support is claimed. An enabled protection request for an unsupported business payload path must refuse before sending/storing that value. Unsupported failure-detail paths must likewise be named; failure message/stack text and IDs/types/queues/search/memo/labels remain deliberately plaintext metadata. No broad all-payload confidentiality claim follows.
    7. Key IDs are immutable identities for exact 32-byte keys; one active ID writes, retained IDs read, duplicate/shadowed IDs and reassignment refuse. No try-all-keys behavior. Operator surfaces show protected/unavailable honestly; external key identity requires verified metadata/fetch rather than guessing.

    These choices unblock a tracked v1 schema/AAD/vector contract and the per-call-site table. That table must bind producer, actual field, consumer, namespace source, boundary kind, inline/external behavior, and protected/unsupported status against current code. SDK implementation and installed/publication acceptance remain unfinished; no runtime, provider, real-key or release effect is selected here.

  16. rmcdaniel commented on Nov 14, 2026

    @rmcdaniel
    MemberAuthor

    The accepted v1 contract is now implemented for independent review in draft PR #399: #399

    It fixes the distinct ProtectedValue schema/fingerprint, exact AAD bytes, deterministic vector and negatives, refusal precedence, key rotation/retry/offload rules, and a source-revision-bound supported/refused call-site table. The PR has no runtime/key-provider effect; SDK, Workflow and Server adoption remains unfinished and is not closed by this deliverable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:cross-repositoryWork spans more than one public repositorykind:featureA public product capability or experience is requestedpriority:P2Normal-priority product workstatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions