Skip to content

Define payload protection v1 contract - #399

Open
rmcdaniel wants to merge 3 commits into
mainfrom
security/217-payload-protection-contract-v1
Open

rmcdaniel wants to merge 3 commits into
mainfrom
security/217-payload-protection-contract-v1

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Addresses the v1 contract prerequisite in #217. SDK, Workflow and Server implementation remains separate unfinished work.

This records the accepted v1 envelope and AAD decisions as a reviewable conformance contract:

  • exact ProtectedValue Avro schema and distinct fingerprint
  • canonical XChaCha20-Poly1305 AAD construction and deterministic vector
  • strict error precedence, key rotation, retry and external-offload rules
  • source-revision-bound supported/refused call-site inventory across SDKs, Workflow and Server
  • stdlib contract tests in ordinary repository CI

This PR performs no runtime encryption, key-provider operation, package release, or live effect.

Local verification:

  • python3 -m unittest tests.test_payload_protection_contract
  • uvx ruff check tests/test_payload_protection_contract.py
  • JSON parsing and git diff --check

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 14, 2026 11:28

@rmcdaniel rmcdaniel left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Mechanical PHP call-site review of contract head 9f20c526f1d9ec6e4c8e592cd352a9655bd056b8, pinned SDK 61477048340e8ab374822381a9018df7db050872. Runtime source bytes were checked at that exact pin. No runtime/protection implementation or effect ran.

The inventory is a future v1 implementation scope. Current PHP accepts only plain fixed-schema Value; none of these rows presently implements protection or an enabled-protection refusal. AvroPayloadCodec::encode returns base64 text of c301 + Value fingerprint + datum; envelope puts that string in {codec:"avro",blob:...}.

Boundary Pinned PHP source and actual wire field Current representation / limit
Workflow input Client::startWorkflow: input; Model/ScheduleAction.php:23: action.input; Worker/WorkflowCommand.php:910: child/continue commands[].arguments; Worker.php:1920,4534: task entry decoding Value envelope; nominated slots may become runtime references.
Workflow result Replayer completion: commands[].result; Client.php:3388: terminal history payload.output/payload.result; Replayer.php:3502: child result decode Value envelope, or hydrated nominated reference.
Ordinary activity input WorkflowCommand::toWire: commands[].arguments; Worker.php:3335,4534: task arguments decode Value envelope or nominated reference. Local/prepared are separate scope.
Ordinary activity result Client::completeActivityTask and continuation at2684: result; Replayer.php:1870,3502: ActivityCompleted payload result Value envelope or nominated reference; continuation command offload has narrower rules.
Signal input Client::signalWorkflow: input; Worker/WorkflowContext.php:887: history value/input/arguments Value envelope/reference. This PHP revision has no WorkflowContext outbound signal-external method: the generic workflow-signal write site is not a PHP implementation claim.
Update input Client::updateWorkflowReceipt: input; Worker.php:3902: accepted-history/task arguments Value envelope or nominated reference.
Update result Worker update completion: commands[].result; Client.php:1170,3376: retained/wait result_envelope or enveloped result Value envelope/reference; current unprotected reader retains raw-result fallback.
Query input Client::queryWorkflow: input; Worker.php:3674: task.query_arguments or task.arguments Value envelope/reference. Worker::queryContext is not a method at this pin; correct the inventory anchor to query execution/decodeArguments.
Query result Client::completeQueryTask: result_envelope plus JSON-encodable plaintext result; Client.php:3376: envelope-first reader plus raw fallback Protecting only result_envelope leaks the display projection. Enabled mode must omit/refuse the plaintext copy and raw fallback. Current offload sets result=null only when replacing result_envelope.
Side-effect result WorkflowCommand side-effect branch: commands[].result = codec.encode(...); Replayer.php:3502 accepts bare string Bare base64 Value single-object bytes, not a JSON envelope; nominated slot can become a reference.
External object RuntimePayloadUploads: upload body is $payload['blob']; exact slots at426, blob extraction at586; RuntimePayloads.php:150 verifies length/hash then hydrates {codec,blob} Object body/hash cover the base64 blob text, not raw decoded Avro bytes. Specify that representation before binding vector object hashes. Negotiated storage and typed refusal remain required.
Heartbeat details — refuse Client::heartbeatActivityTask: raw JSON details; Worker.php:4133 checks local heartbeat encodability but records raw details No portable protected envelope; enabled-protection refusal is appropriate.
Streams — refuse Client::appendMessageStream: enveloped input; Client.php:941 + Model/WorkflowStreamAppendItem.php:22: items[].payload/payload_reference; workflow-attached stream directives also exist Existing Avro/reference representations do not admit v1 stream semantics. Refusal is a scope decision; not all these fields are raw JSON.
Nexus — refuse Client::serviceOperationBody: bare encoded arguments plus payload_codec; WorkflowCommand.php:906: enveloped request_payload Existing Value slots do not establish caller/target AAD authority.
Local/prepared — refuse WorkflowCommand::resolveLocalActivity: argument/result envelopes; PreparedLocalActivityRunner.php:154: report.result; uploads at452: descriptor.arguments/report.result Some paths already use portable Value envelopes. That does not qualify all persistence/retry paths for protection; refusal is intentional scope.
Failure details — refuse Client::failActivityTask: optional Value failure.details, plaintext message/type/stack/diagnostics; upload slots at445 include exception.details Ad hoc details are not an admitted v1 boundary. Do not globally encrypt every existing Avro slot.
Unmapped external tasks / raw JSON — refuse Outbound payload validation, AvroPayloadCodec.php:57 validates fixed framing/schema for declared payloads; raw projections/heartbeat are separate surfaces No extra PHP external-task boundary is proven here. Unknown ProtectedValue fingerprint currently fails unsupported_payload_schema, not the future contract error payload_envelope_unsupported.

The concrete inventory corrections are the missing queryContext anchor, query result's plaintext projection/raw fallback, and base64-text external-object representation. A global codec-only switch cannot implement this scope: WorkflowCommand.php:927 also encodes upsert_memo.entries as Avro while memo remains plaintext metadata, and several intentionally refused paths already use Avro. Conformance must exercise boundary-aware protection/refusal and exact transport representations. This is PHP source feedback only, not approval of other bindings or cryptographic implementation.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Mechanical Python source audit for 949d38bb59aa7221f1e05f1309f3a6f0f53c8d96, against PR399 head 9f20c526f1d9ec6e4c8e592cd352a9655bd056b8. This is a transport/call-site review, with no crypto, SDK, Worker or Server execution. No payload-protection implementation exists at this Python pin: the protected / unsupported_refuse statuses are proposed v1 requirements, not implemented capability or refusal claims.

All paths below are relative to that immutable SDK commit. Inline means the existing Avro Value single-object encoded as the blob text. The common writers are client _payload_envelope, workflow _payload_envelope, and serializer envelope/offload. They emit {codec:"avro",blob} or configured direct-storage {codec,external_storage}. Runtime offload can replace an eligible envelope with {codec,external_payload}. These are plain Value transports today.

Boundary Exact write/read anchors and actual fields Current Python behavior
Workflow input client.py:4052 POST /workflows body input; client.py:1383,5403 schedule action.input; workflow.py:1268 child command arguments; workflow.py:1140 continue-as-new command arguments; worker task.arguments decoding through worker.py:3921 Inline Value or configured external reference. Current diagnostic kinds are workflow_input, schedule_input, child_workflow_input, continue_as_new_input; they are not existing AAD and must explicitly map to the proposed single workflow_input kind.
Workflow result workflow.py:1000 completion command result; client.py:4109 describe output_envelope; client.py:5358 completed-history payload.output; workflow.py:3248 child/history result or output Inline Value or configured reference, decoded through the official serializer.
Ordinary activity input workflow.py:717 schedule command arguments; worker.py:3516 received task.arguments Inline Value or configured reference. No local/prepared protection is implied.
Ordinary activity result client.py:6789 activity completion result; workflow.py:3248 ActivityCompleted result/output Inline Value or configured reference.
Signal input client.py:4873,4917 signal body input; workflow.py:3274 replay value, then input, then arguments Inline Value or reference when nonempty arguments are supplied. None/empty arguments omit input. Python has no workflow-authored signal-external command at this pin; the table's workflow signal command write site is not Python implementation evidence (shared223 remains open).
Update input client.py:5157 update body input; workflow.py:3304 update-history arguments Inline Value or reference for supplied nonempty arguments; absent/empty input is omitted.
Update result workflow.py:1071 complete-update command result; client.py:2959 receipt result_envelope; client.py:5190 retained outcome Inline Value or reference. The common response hydration currently uses the envelope only when the parallel result projection is null.
Query input client.py:4967 query body input; Worker list-payload decoding worker.py:3921 Inline Value or reference for nonempty arguments; absent/empty input is omitted.
Query result client.py:6591 completion result_envelope, plus optional plaintext result; response hydration client.py:2959 Two representations today: authoritative Avro envelope plus JSON-compatible plaintext projection. See the required implementation obligation below.
Side-effect result workflow.py:1177 record-side-effect result, helper workflow.py:554; history decoding workflow.py:3248 Inline base64 Value blob string, or direct-storage envelope. Runtime offload normalizes the string and can replace result with an external-payload envelope (client.py:2536). This row is not uniformly an inline {codec,blob} dictionary.
External object serializer.py:287 direct upload, client.py:2570 runtime upload; serializer.py:448 and client.py:2790 fetch/resolve Object bytes are the UTF-8/base64 blob text, with size/hash checked before Value decode. This matches the vector's external_object_ascii_bytes framing. Protection-before-threshold/decrypt-after-integrity is new implementation work, not currently implemented.

Unsupported-v1 boundaries still have current transports and do not presently refuse merely because a protection mode was selected; Python has no such mode yet:

Boundary Source/field Current Python behavior
Heartbeat details client.py:7050, heartbeat body details Raw JSON dictionary; no portable Value envelope.
Stream items/input client.py:4526 items[].payload; client.py:4943 message-stream input; workflow.py:2638 workflow_stream.items[].payload inside side-effect directive Value envelopes/references are already transported on some stream paths. V1 refusal must be explicit, not inferred from absence of an envelope.
Nexus request/result client.py:3318,3356 outgoing service arguments; workflow.py:1371 start_service_operation.request_payload; workflow.py:3051 result envelope Value blob/envelope/reference paths exist; namespace/AAD protection is not implemented.
Local/prepared activity workflow.py:861,889, local arguments; existing local result/marker handling Plain Value envelope paths exist. Their unsupported-v1 disposition needs explicit gating before encoding/offload.
Other external task No blanket ordinary-activity alias can be established from these Python call sites Keep unmapped paths refused under the proposed mode; do not infer support from a recursively discovered envelope.
Failure details client.py:6845 body failure.details; failure normalization/budgeting precedes request send Details may already be an Avro envelope or an omitted/unencodable marker; messages/stacks/types remain JSON metadata. This is not uniformly raw JSON, and generic envelope interception must not silently protect only part of it.
Raw JSON / generic binary serializer.py:193 codec validation; existing HTTP document transport Non-Avro durable payload codecs already refuse. Avro Value.bytes remains a legitimate typed value; that is distinct from an untyped generic binary transport. No protected-mode refusal exists yet.

Two concrete corrections/obligations should be explicit in the inventory/contract before implementation is treated as complete:

  1. Scope workflow signal command by binding; it is absent in Python. Shared223 is the missing capability, not an encryption hook that already exists.
  2. A protected query/update-result implementation must suppress plaintext result projections and require the authoritative envelope to be verified even when a non-null plain result is present. Today complete_query_task deliberately emits the projection for JSON-compatible values (client.py:6625), and _hydrate_result_envelopes bypasses envelope decode when that projection is non-null (client.py:2965). Protecting only result_envelope would leak the value and leave a strict-reader bypass. The table should map both write and read sites rather than only the envelope helper.

The existing helper kind values are warning/diagnostic labels, not authenticated boundaries. Likewise, strict protection must deliberately define the omitted-empty-argument cases above and gate unsupported envelope-bearing APIs before uploads; a generic serializer wrapper alone does not deliver the proposed table.

@rmcdaniel

rmcdaniel commented Nov 14, 2026 •

Copy link
Copy Markdown
Member Author

Pinned Rust mechanical call-site review at 1129d797f266e518eedf642460c89b412dca9dad for PR399's source inventory. This verifies existing serializers and carriers only; the pinned SDK has no payload-protection implementation or installed protection result.

Two current anchors need correction: src/metrics/routes.rs classifies routes and is not the query-result serializer; use Client::complete_query_task_with_envelope / query_workflow_response in src/lib.rs. src/run_activities.rs reads heartbeat visibility metadata; it is not the heartbeat writer. Use Client::heartbeat_activity_task_value and ActivityContext::heartbeat in src/lib.rs, retaining run-activities as an optional raw read anchor.

Boundary Exact Rust write/read anchors at the pinned source Actual payload slots
workflow input Client::start_workflow_with_options, ChildWorkflowCall::poll_avro_value L16660, WorkflowContext::continue_as_new_command L12242; entry decode_task_avro_arguments L17036 Start body input; child/continue commands arguments; received task arguments. Each contains the normalized complete fixed Avro Value envelope. No typed schedule-creation authoring helper exists at this pin; do not invent a Rust schedule-write anchor from generic raw transport.
workflow result Worker::finish_workflow_task_execution, WorkflowDescription::decode_payloads L6919, WorkflowHandle::result_avro_value_target L6584, child/history readers history_payload_fields L17153 commands[].result for complete_workflow; describe output_envelope and decoded output; history WorkflowCompleted.payload.output, child ChildRunCompleted.payload.result/output.
ordinary activity input ActivityCall::poll_avro_value, Worker::execute_session_activity_task_inner, decode_task_avro_arguments Ordinary schedule_activity.arguments; received task arguments. The same function also authors local commands, which are a separate unsupported protection boundary.
ordinary activity result Client::complete_activity_task, activity_outcome L18934 Completion body result; replay ActivityCompleted.payload.result, with payload_codec.
signal input Client::signal_workflow_target, decode_resume_signal L17046, history_signal_events L19577 Body input; task signal_arguments; SignalReceived/SignalApplied payload value/input/arguments, or matched export signals[].arguments. No workflow-originated cross-workflow signal command helper is present at this pin.
update input Client::update_workflow_response, Worker::execute_update_task around L10488 Body input; accepted-history/task arguments decoded before validation/handler execution.
update result Worker::execute_update_task, completed_update_avro_value / Client::update_outcome / submitted_update_value complete_update.result; client receipt/outcome result_envelope, with JSON result fallback in Value-facing readers.
query input Client::query_workflow_response, Worker::execute_query_task_with_authority Body input; query task arguments.
query result Client::complete_query_task_with_envelope, query_workflow_target L3762 / typed query result L3750 Completion sends both plaintext JSON result and result_envelope; typed client requires the envelope, Value client falls back to result. Protection must suppress/refuse the plaintext duplicate and raw fallbacks when enabled, not encrypt only the envelope.
side-effect result WorkflowContext::side_effect / side_effect_avro_value, recorded-command/history readers record_side_effect.result; replay SideEffectRecorded.payload.result. Whole typed Value before the callback result is converted for the handler.
external carrier Client::externalize_runtime_payloads, select, replace, payload_paths, Reference::parse / resolve_runtime_payloads_with_cache / fetch_runtime_payload Inline {codec:"avro",blob} becomes {codec:"avro",external_payload:{schema,codec,reference_id,size_bytes,sha256}}. Exact ASCII blob bytes are size/hash bound, fetched under configured namespace and rebuilt into an inline envelope before typed decode. Offload eligibility also includes unsupported rows, so this transport whitelist is not the protection whitelist.

For unsupported rows, add the actual Rust anchors rather than treating an Avro carrier as protection admission:

  • Heartbeats: Client::heartbeat_activity_task_value sends raw JSON details after AvroValue::from_serialize(...).into_json(), with no payload envelope. ActivityContext::heartbeat also has local/session/cooperative dispatch; run_activities.rs is raw progress visibility only.
  • Stream items: Client::append_message_stream has input; WorkflowStreamAppendItem::new has payload / payload_envelope, while from_reference preserves an opaque caller reference. These remain the separately unsupported stream contract.
  • Nexus: src/nexus.rs::NexusServiceCall::poll_avro_value authors start_service_operation.request_payload; recorded service outcomes read response_payload. Codec reuse does not settle namespace/AAD semantics.
  • Local/prepared: ActivityCall's record_local_activity and src/local_activity.rs use arguments/results plus per-attempt heartbeat reports. runtime_uploads.rs::payload_paths explicitly visits those nested details; ordinary activity support must not silently admit them.
  • Failure details: Client::fail_activity_task_with_failure, fail_workflow_task_with_failure, and src/failure_reporting.rs preserve bounded details alongside plaintext message/type/stack. Keep the separate refusal boundary.
  • Raw/generic: RawRequest::json, Client::control_plane_request / worker_plane_request / send_raw_request deliberately never rewrite, externalize or interpret the body, and return raw bytes/JSON. They cannot inherit a typed protection boundary automatically.

I read these pinned source paths and the exact call-site inventory; no compiler, SDK, Server, network payload or cryptographic implementation ran. The query plaintext shadow/fallback and the broader external-upload whitelist should be explicit implementation obligations in the inventory before any future protection claim.

@rmcdaniel

rmcdaniel commented Nov 14, 2026 •

Copy link
Copy Markdown
Member Author

Pinned Workflow persistence audit for 8bb05a6368b4046602fcae2fdd3a075f36dbf3ae, against the call-site inventory at .github 9f20c526f1d9ec6e4c8e592cd352a9655bd056b8.

The inventory is a proposed protection contract, not evidence of encryption in this revision. Embedded durable values currently use the codec blob plus a separate codec field. The default Avro blob is base64 single-object Value encoding (Avro.php:21). No embedded ProtectedValue writer exists at this pin.

For externalized values, a DB text field contains dw-external-payload:v1: plus the encoded reference envelope; a history payload instead contains {codec, external_storage}. Inline history retains the codec blob string. This distinction is defined by ExternalPayloads:17, historyValue:161 and wireEnvelope:143.

Inventory row Actual embedded persistence / reader at the pinned revision
Workflow input workflow_runs.arguments / payload_codec; direct start also retains workflow_commands.payload. WorkflowStub:1872, start command:2158. Child start uses the child's namespace and stores workflow_runs.arguments at WorkflowExecutor:2921; continue-as-new at 3932. These are codec bytes or stored external references, not an arguments Value field in WorkflowStarted: that event chiefly retains command/definition metadata.
Workflow result workflow_runs.output / output_payload_codec, plus WorkflowCompleted.payload.output / payload_codec, written by WorkflowExecutor::completeRun:4387. The inventory's WorkflowStub anchor is a reader, not the completion writer. Inline blob or external reference as above.
Activity input activity_executions.arguments / payload_codec, plus ActivityScheduled.payload.activity.arguments in the execution snapshot: WorkflowExecutor:2686, ActivitySnapshot:52. workflow_tasks.payload carries activity_execution_id, not the argument bytes. Both inline/offloaded forms exist.
Activity result Correct the writer anchor: ActivityOutcomeRecorder:235 and 342 write activity_executions.result / payload_codec, and ActivityCompleted.payload.result, also duplicated in payload.activity.result. src/V2/Events/ActivityCompleted.php is a post-commit lifecycle notification containing IDs/timestamp; it has no result payload. Inline/offloaded forms exist.
Signal input workflow_signals.arguments / payload_codec, command payload, and SignalReceived.payload.arguments / payload_codec: WorkflowStub::recordAcceptedSignal:4572, history writer:3449. Ordinary local arguments serialize inline; an admitted pre-encoded payload/reference can be supplied. This path does not independently call namespace-policy externalization for ordinary local signal arguments.
Update input workflow_updates.arguments / payload_codec; UpdateAccepted.payload.arguments, later UpdateApplied.payload.arguments: WorkflowStub:2857, 2927, WorkflowExecutor:6727. This admission serializes and size-checks the inline codec blob; it does not externalize it. Do not infer symmetric offload from update result.
Update result Add embedded anchors: WorkflowExecutor:6746 writes workflow_updates.result and UpdateCompleted.payload.result, inline or externalized using the run namespace. WorkflowUpdate:120 resolves/decodes the retained result.
Query input Add embedded anchors: WorkflowStub::queryWithArguments:820, QueryStateReplayer::query:34. Arguments are validated PHP values passed to a read-only replayed workflow method; no embedded query-task argument DB/history field or portable query-input envelope is written. Native query-task transport is a distinct path.
Query result QueryStateReplayer:40 returns the PHP value directly; QueryResponse:68 places it in the response's result. No embedded durable query-result field or automatic codec/offload boundary exists. Add this difference rather than claiming existing embedded complete-query-task storage.
Side-effect result WorkflowExecutor::recordSideEffect:5582 writes SideEffectRecorded.payload.result as inline codec bytes or external reference. No separate side-effect-result row.
External object for a supported boundary ExternalPayloads::externalize:36 passes codec bytes to ExternalPayloadStorage::store:26. The reference contains URI, codec, size and SHA-256. fetch:38 verifies exact size/hash before returning bytes for decode. Future protection must operate on those bytes before the offload decision; current external storage does not itself encrypt.
Activity heartbeat details — unsupported Activity::heartbeat:109, ActivityHeartbeatHistory:37 write activity_attempts.last_heartbeat_progress (JSON), optionally ActivityHeartbeatRecorded.payload.progress. HeartbeatProgress:23 bounds raw structured progress. No Value/protection/offload envelope.
Message stream item — unsupported MessageStream::send:159, MessageService:99: workflow_messages.payload_reference plus JSON metadata. Payload bytes live outside the table; the reference is not proof that it uses admitted ExternalPayloads/Value framing.
Nexus request/result — unsupported Add the writer WorkflowExecutor::recordServiceOperationEvent:6135. History service events contain request_payload, response_payload and response_or_failure_surface (6332); requests can be serialized/offloaded. ServiceCallWorkflowDelivery:243 copies result envelopes into workflow delivery history. ServiceOperationResult is an in-memory surface, not a persistence writer. Existing codec/reference use does not establish the missing caller/target AAD authority.
Local/prepared activity — unsupported LocalActivityExecutor:1041 writes execution arguments, 1281 writes results and ActivityCompleted history; these also have codec/external-reference forms. Prepared completion normalizes report.result through 746. Shared storage does not make these ordinary durable activity admission paths.
External task outside ordinary activity — unsupported ExternalTaskInput:41 parses task/workflow/lease/payloads transport objects, with only activity/workflow task kinds admitted. It is not another generic payload DB column. Ordinary activity completion reaches the result row above; an unproven external-task body has no admitted protected boundary.
Failure details — unsupported FailureFactory::make:46 builds plaintext exception class/message/file/line/trace fields stored in workflow_failures; WorkflowFailure.message_full preserves overflow bytes (WorkflowFailure:15). Activity execution exception snapshots are additionally codec-serialized at ActivityOutcomeRecorder:265. Neither is a universally admitted failure-details protection boundary.
Raw JSON / generic binary — unsupported No single stored field or namespace authority exists. JSON history metadata, task payloads and arbitrary raw HTTP bodies are not automatically Value payloads; bytes inside an admitted Avro BytesValue are a different, typed case. Refusal must occur at each unadmitted boundary, never by wrapping an arbitrary transport body.

Required inventory corrections: replace the lifecycle activity-result anchor with the actual outcome writer; add update-result and embedded query anchors; record the inline-only update-input and local-signal/offload distinction; preserve the DB-reference versus history-reference forms. These are mechanical source observations only. No encryption implementation, installed producer or release is qualified by this review.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Native corrections at pinned Server 00f20996a18d4795fa0a09319470df1f5160c079:

Replace continuing native producer anchors under server:app/ with the Rust anchors below. HTTP field names differ from persisted field names; do not infer them from the semantic boundary name.

Boundary Native source anchors under rust/src/runtime/ HTTP/wire field Persisted representation / history
Workflow input execution.rs::Runtime::start, store.rs::Store::start_in; schedule_storage.rs::Store::create_schedule, schedule_execution.rs::Store::start_scheduled; children.rs::Store::start_child, continuations.rs::Store::continue_run Direct start input; scheduled action action.input; child/continue commands arguments; claimed workflow task arguments workflow_runs.arguments, with payload_codec=avro; start workflow_commands.payload; schedules retain JSON workflow_schedules.action.input and ScheduleCreated.payload.action.input. Child/continue write the successor run's arguments. Do not invent a uniform top-level WorkflowStarted.arguments field.
Workflow result store.rs::Store::complete_or_checkpoint_in, children.rs::Store::resolve_child complete_workflow command result workflow_runs.output; WorkflowCompleted.payload.output; parent ChildRunCompleted.payload.output and .result.
Ordinary activity input store.rs::Store::apply_non_terminal (schedule_activity), Store::activity_payload_with_completion Command arguments; claimed activity task arguments activity_executions.arguments; ActivityScheduled.payload.activity.arguments.
Ordinary activity result execution.rs::Runtime::complete_activity, store.rs::Store::complete_activity_in Activity-complete body result activity_executions.result; ActivityCompleted.payload.result (with payload_codec=avro).
Signal input execution.rs::Runtime::signal_command, store.rs::Store::signal, Store::record_accepted_signal, Store::apply_task_signal Control-plane body input; delivered signal/history arguments workflow_signal_records.arguments, workflow_commands.payload; SignalReceived.payload.arguments; SignalApplied.payload.value is a separately encoded derived value, not a copied arguments field.
Update input execution.rs::Runtime::update_command, updates.rs::Store::update_workflow Control-plane body input; worker/update delivery arguments workflow_updates.arguments, workflow_commands.payload; update acceptance/applied payload arguments.
Update result updates.rs::Store::apply_update complete_update command result workflow_updates.result; UpdateCompleted.payload.result.
Query input execution.rs::Runtime::query_workflow, queries.rs::Store::query_workflow Control-plane body input; query claim query_arguments (separate from workflow_arguments) Query task JSON at dw_query_cache.value.query_arguments; no workflow-history event is created by the query.
Query result queries.rs::Store::finish_query / Store::query_outcome Worker body result_envelope; current plain-JSON alias result is a separate transport dw_query_cache.value.result_envelope and optional .result; response exposes those fields. No workflow-result row/history write. With protection enabled, the raw result alias needs the unsupported/plaintext refusal policy; it must not bypass the protected envelope.
Side-effect result mod.rs::side_effect_result, store.rs::Store::apply_non_terminal record_side_effect.result is normally a bare Avro blob string with optional payload_codec=avro; an ordinary Avro envelope is also accepted SideEffectRecorded.payload.result is rendered by wire(...). No dedicated result column.

Representation corrections:

  • mod.rs::envelope / wire define inline { "codec":"avro", "blob":"<base64 Avro single-object payload>" }; relevant TEXT columns retain the blob string rather than that JSON wrapper. This snapshot does not implement the proposed payload-protection record itself.
  • Native external wire references use { "codec":"avro", "external_payload": <Reference> }, not external_storage or payload_ref. external_resolution.rs::{PreparedExternal::stored,stored_envelope} persist dw-native-external-payload-v1: plus reference JSON in the same payload column and reconstruct that wire reference. external_transport.rs and external_payload own upload/fetch integrity; storage location is not another boundary kind.
  • The existing external object's bytes are read as the UTF-8 Avro blob string by PreparedExternal::blob before base64/Value decoding. Do not silently describe this snapshot as storing raw binary Avro single-object bytes.
  • Reference retention is path-specific. Direct start, signal, ordinary activity input/result and workflow-result paths explicitly preserve prepared references. Update/query input resolve a reference to the blob before retaining their inline arguments. prepare_command_payloads selects only complete_workflow.result and schedule_activity.arguments; scheduled starts, child/continue, update-result and side-effect validators use the inline envelope path. Query completion accepts the inline result_envelope plus its separate raw JSON alias. One logical AAD kind does not establish identical offload support at every call site.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source mapping correction is pushed at 1ad789f.

This binds every protected boundary to exact pinned SDK, embedded Workflow, and native Server anchors plus its wire/stored fields and inline/external representation. It also records the strict-mode obligations discovered by the independent PHP, Python, Rust, Workflow, and Server audits: suppress query/update plaintext shadows and raw fallbacks; refuse unsupported paths before existing codec/upload hooks; distinguish embedded, runtime, and native external references; and treat external object bytes as the 228 ASCII bytes of canonical base64, not the 169 decoded Avro-frame bytes.

The conformance tests now enforce complete five-binding mappings, corrected native/embedded anchors, plaintext-shadow closure, and exact external framing. python3 -m unittest tests.test_payload_protection_contract tests.test_regression_corpus passes 47 tests; JSON validation and git diff --check pass.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security contract/source review PASS for exact 1ad789f6b82824f7bfa8bd3cc27a0d46e03ec227.

The v1 contract is now unambiguous at the security and transport boundaries:

  • envelope recognition is the distinct Avro single-object fingerprint, so ordinary user maps cannot be reinterpreted;
  • AAD is a closed byte grammar over version, exact algorithm, immutable key ID, authoritative namespace and boundary kind;
  • strict/migration refusal precedence, key rotation and cross-process retry preservation are explicit;
  • protection precedes offload, and the external size/hash correctly bind the canonical ASCII base64 object rather than decoded Avro bytes;
  • all supported and refused paths are mapped to exact pinned repository revisions, actual wire/stored/cache/history fields and inline/external representations;
  • query/update plaintext projections and raw fallbacks are explicitly forbidden in strict mode, while streams, Nexus, heartbeats, local/prepared work, failure details and raw transports refuse before codec/upload hooks.

I independently recomputed the ProtectedValue Avro parsing-canonical fingerprint (0ba70c3fd6c311ca), decoded the complete outer frame, recomputed its external 228-byte ASCII identity/SHA-256, reconstructed the canonical AAD, and decrypted the vector with XChaCha20-Poly1305 (HChaCha20 subkey plus IETF ChaCha20-Poly1305); the plaintext matches the complete declared Value frame byte-for-byte. The focused contract tests pass.

The current required-check failure is the already-tracked retired PHP setup asset (411a... HTTP 404), not this contract. Do not merge until the normally reviewed central setup repair is active and a fresh required check is terminal. This verdict defines an implementation boundary; it does not qualify any encryption implementation, key provider, runtime effect or partially protected deployment.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant