Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f250460
Reproduce select member cancellation lost at the HTTP boundary
rmcdaniel Oct 21, 2026
885114b
Retain select cancellation identity through worker HTTP validation
rmcdaniel Oct 21, 2026
892c2c7
Merge remote-tracking branch 'origin/main' into fix/583-selection-can…
rmcdaniel Oct 21, 2026
a19ff2e
Use the actual expired-claim response contract in the HTTP regression
rmcdaniel Oct 21, 2026
d055dc5
Prepare Server 2.10.1 and Helm 0.2.3 for select cancellation recovery
rmcdaniel Oct 21, 2026
171d253
Reuse the workflow stream validation condition
rmcdaniel Oct 21, 2026
5527ddd
Merge remote-tracking branch 'origin/main' into fix/583-selection-can…
rmcdaniel Oct 21, 2026
ba8a4f6
Merge remote-tracking branch 'origin/main' into fix/583-selection-can…
rmcdaniel Oct 21, 2026
cb77530
Merge remote-tracking branch 'origin/main' into fix/583-selection-can…
rmcdaniel Oct 21, 2026
efa0b17
Merge remote-tracking branch 'origin/main' into fix/583-selection-can…
rmcdaniel Oct 21, 2026
f399995
security: require patched PHP and reduce request startup exposure
rmcdaniel Oct 21, 2026
209b771
Merge commit 'efa0b17858c33bb0b1df7b73b666dbef37d04d70' into fix/591-…
rmcdaniel Oct 21, 2026
5d8735c
Build patched Apache PHP from verified upstream release
rmcdaniel Oct 21, 2026
fde9027
Merge remote-tracking branch 'origin/main' into fix/591-patched-php
rmcdaniel Oct 21, 2026
404ac64
Enforce HTTPS source redirects and clarify immutable PHP build inputs
rmcdaniel Oct 21, 2026
7257518
Sort the unchanged PHP build dependency list
rmcdaniel Oct 21, 2026
6a1adf1
Merge remote-tracking branch 'origin/main' into fix/591-patched-php
rmcdaniel Oct 21, 2026
0cb1a15
Merge remote-tracking branch 'origin/main' into fix/591-patched-php
rmcdaniel Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,110 @@ RUN git clone --depth 1 --branch "${PHPREDIS_VERSION}" https://github.com/phpred
exit 1; \
fi

FROM php:8.3-apache AS base
# Use the signed upstream fix while Docker Official Image publication catches up.
# Configure options match docker-library/php at f6048b5f6e3c2202ee082665a4ccc0d90de291a3.
FROM php@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS patched-php

LABEL dev.durable-workflow.php.source.version="8.3.36" \
dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd"

ARG PHP_BUILD_JOBS=2
ENV PHP_VERSION="8.3.36" \
PHP_URL="https://www.php.net/distributions/php-8.3.36.tar.xz" \
PHP_ASC_URL="https://www.php.net/distributions/php-8.3.36.tar.xz.asc" \
PHP_SHA256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd"

COPY docker/php-keyring.gpg /tmp/php-keyring.gpg

RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
apache2-dev \
autoconf \
ca-certificates \
curl \
dpkg-dev \
file \
g++ \
gcc \
gnupg \
libargon2-dev \
libc-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
make \
pkg-config \
re2c \
xz-utils \
zlib1g-dev; \
printf '%s %s\n' \
'43fcb3eaf65536cb0833c229b492c70cfad8562138164dd13673e08925d6c56c' \
/tmp/php-keyring.gpg | sha256sum --check -; \
curl --proto '=https' --proto-redir '=https' --fail --location --show-error --silent \
--output /usr/src/php.tar.xz "${PHP_URL}"; \
printf '%s %s\n' "${PHP_SHA256}" /usr/src/php.tar.xz | sha256sum --check -; \
curl --proto '=https' --proto-redir '=https' --fail --location --show-error --silent \
--output /tmp/php.tar.xz.asc "${PHP_ASC_URL}"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --import /tmp/php-keyring.gpg; \
gpg --batch --verify /tmp/php.tar.xz.asc /usr/src/php.tar.xz; \
gpgconf --kill all; \
rm -rf "${GNUPGHOME}" /usr/src/php; \
mkdir -p /usr/src/php; \
tar --extract --file /usr/src/php.tar.xz --xz --strip-components=1 --directory /usr/src/php; \
cd /usr/src/php; \
gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
deb_multiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \
export CFLAGS="${PHP_CFLAGS}" CPPFLAGS="${PHP_CPPFLAGS}" LDFLAGS="${PHP_LDFLAGS}" \
PHP_BUILD_PROVIDER='https://github.com/docker-library/php' PHP_UNAME='Linux - Docker'; \
./configure \
--build="${gnu_arch}" \
--sysconfdir="${PHP_INI_DIR%/php}" \
--with-config-file-path="${PHP_INI_DIR}" \
--with-config-file-scan-dir="${PHP_INI_DIR}/conf.d" \
--enable-option-checking=fatal --with-mhash --with-pic \
--enable-mbstring --enable-mysqlnd --with-password-argon2 --with-sodium=shared \
--with-pdo-sqlite=/usr --with-sqlite3=/usr --with-curl --with-iconv \
--with-openssl --with-readline --with-zlib --disable-phpdbg --with-pear \
--with-libdir="lib/${deb_multiarch}" --disable-cgi --with-apxs2; \
make -j "${PHP_BUILD_JOBS}"; \
make install; \
cp php.ini-development php.ini-production "${PHP_INI_DIR}/"; \
strip --strip-all /usr/local/bin/php /usr/lib/apache2/modules/libphp.so; \
test "$(php -r 'echo PHP_VERSION;')" = "${PHP_VERSION}"; \
rm -rf /usr/src/php /var/lib/apt/lists/* /tmp/php.tar.xz.asc /tmp/php-keyring.gpg

FROM php@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS base

# Replace every PHP runtime/build artifact, including the Apache module and
# extension sources used below. A changed image environment is not a PHP update.
COPY --from=patched-php /usr/local/bin/php /usr/local/bin/php
COPY --from=patched-php /usr/local/bin/php-config /usr/local/bin/php-config
COPY --from=patched-php /usr/local/bin/phpize /usr/local/bin/phpize
COPY --from=patched-php /usr/local/lib/php/ /usr/local/lib/php/
COPY --from=patched-php /usr/local/include/php/ /usr/local/include/php/
COPY --from=patched-php /usr/lib/apache2/modules/libphp.so /usr/lib/apache2/modules/libphp.so
COPY --from=patched-php /usr/src/php.tar.xz /usr/src/php.tar.xz
COPY --from=patched-php /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini-development
COPY --from=patched-php /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini-production

ENV PHP_VERSION="8.3.36" \
PHP_URL="https://www.php.net/distributions/php-8.3.36.tar.xz" \
PHP_ASC_URL="https://www.php.net/distributions/php-8.3.36.tar.xz.asc" \
PHP_SHA256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd"

LABEL dev.durable-workflow.php.source.version="8.3.36" \
dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd"

# Both web and CLI roles require the fixed PHP runtime, including cached builds.
RUN php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3 || PHP_VERSION_ID < 80336) { \
fwrite(STDERR, "Patched PHP 8.3.36 or later on the 8.3 line is required; found ".PHP_VERSION.PHP_EOL); \
exit(1); }'

COPY --from=phpredis-source /phpredis /usr/src/php/ext/redis

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Laravel applications can also run the engine directly in

## Run Server

For the 21 October 2026 critical PHP update, follow the
[PHP runtime security notice](docs/security/php-runtime-2026-10-21.md) and verify
the patched runtime in every deployed role.

The published Compose stack starts Server with MySQL, Redis, a queue worker,
and the scheduler. It bootstraps the database and the default namespace before
accepting traffic.
Expand Down
141 changes: 2 additions & 139 deletions app/Http/Controllers/Api/WorkerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
use App\Support\WorkflowQueryTaskBroker;
use App\Support\WorkflowReplayEligibility;
use App\Support\WorkflowStreamCommandProcessor;
use App\Support\WorkflowStreamService;
use App\Support\WorkflowTaskCompletionValidation;
use App\Support\WorkflowTaskLeaseRecovery;
use App\Support\WorkflowTaskPoller;
use App\Support\WorkflowUpdateValidationTaskBroker;
Expand Down Expand Up @@ -1659,144 +1659,7 @@ private function mutateWorkflowTaskCommands(Request $request, string $taskId, bo
'message_stream_waits.*.after_position' => ['required', 'integer', 'min:0'],
]);

$completionRules = [
'lease_owner' => ['required', 'string'],
'workflow_task_attempt' => ['required', 'integer', 'min:1'],
'commands' => ['required', 'array', 'min:1'],
'commands.*.type' => ['required', 'string'],
'commands.*.cancellation_scope_id' => ['sometimes', 'string', 'min:1', 'max:255'],
'commands.*.cancellation_cleanup' => ['sometimes', 'required', 'array:scope_id,request_id,delivery_history_event_id'],
'commands.*.cancellation_cleanup.scope_id' => ['sometimes', 'required', 'string', 'max:255'],
'commands.*.cancellation_cleanup.request_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'],
'commands.*.cancellation_cleanup.delivery_history_event_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'],
'commands.*.result' => ['nullable'],
'commands.*.activity_type' => ['nullable', 'string'],
'commands.*.arguments' => ['nullable'],
'commands.*.connection' => ['nullable', 'string'],
'commands.*.queue' => ['nullable', 'string'],
'commands.*.retry_policy' => ['nullable', 'array'],
'commands.*.retry_policy.max_attempts' => ['nullable', 'integer', 'min:1'],
'commands.*.retry_policy.backoff_seconds' => ['nullable', 'array'],
'commands.*.retry_policy.backoff_seconds.*' => ['integer', 'min:0'],
'commands.*.retry_policy.non_retryable_error_types' => ['nullable', 'array'],
'commands.*.retry_policy.non_retryable_error_types.*' => ['string'],
'commands.*.retry_policy.*' => ['nullable'],
'commands.*.start_to_close_timeout' => ['nullable', 'integer', 'min:1'],
'commands.*.schedule_to_start_timeout' => ['nullable', 'integer', 'min:1'],
'commands.*.schedule_to_close_timeout' => ['nullable', 'integer', 'min:1'],
'commands.*.heartbeat_timeout' => ['nullable', 'integer', 'min:1'],
'commands.*.execution_mode' => ['nullable', 'string', 'in:local'],
'commands.*.outcome' => ['nullable', 'string', 'in:completed,failed,timed_out,cancelled'],
'commands.*.attempts' => ['nullable', 'array', 'max:100'],
'commands.*.attempts.*.attempt_id' => ['nullable', 'string', 'max:255'],
'commands.*.attempts.*.attempt_number' => ['required', 'integer', 'min:1'],
'commands.*.attempts.*.outcome' => ['required', 'string', 'in:completed,failed,timed_out,cancelled'],
'commands.*.attempts.*.duration_ms' => ['nullable', 'integer', 'min:0'],
'commands.*.attempts.*.message' => ['nullable', 'string'],
'commands.*.attempts.*.exception_type' => ['nullable', 'string', 'max:255'],
'commands.*.attempts.*.non_retryable' => ['nullable', 'boolean'],
'commands.*.attempts.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'],
'commands.*.attempts.*.retry_reason' => ['nullable', 'string', 'in:failure,timeout,cold_replay'],
'commands.*.attempts.*.backoff_seconds' => ['nullable', 'integer', 'min:0'],
'commands.*.attempts.*.heartbeats' => ['nullable', 'array', 'max:1000'],
'commands.*.attempts.*.heartbeats.*.details' => ['nullable', 'array'],
'commands.*.attempts.*.heartbeats.*.elapsed_ms' => ['nullable', 'integer', 'min:0'],
'commands.*.attempts.*.heartbeats.*.*' => ['nullable'],
'commands.*.attempts.*.*' => ['nullable'],
'commands.*.worker_session' => ['nullable', 'array'],
'commands.*.worker_session.session_id' => ['nullable', 'string', 'max:255'],
'commands.*.worker_session.connection' => ['nullable', 'string', 'max:255'],
'commands.*.worker_session.queue' => ['nullable', 'string', 'max:255'],
'commands.*.worker_session.requirements' => ['nullable', 'array'],
'commands.*.worker_session.requirements.*' => ['string', 'max:255'],
'commands.*.worker_session.lease_seconds' => ['nullable', 'integer', 'min:1'],
'commands.*.worker_session.ttl_seconds' => ['nullable', 'integer', 'min:1'],
'commands.*.worker_session.max_concurrent_activities' => ['nullable', 'integer', 'min:1'],
'commands.*.worker_session.create_if_missing' => ['nullable', 'boolean'],
'commands.*.worker_session.allow_reacquire_after_failure' => ['nullable', 'boolean'],
'commands.*.execution_timeout_seconds' => ['nullable', 'integer', 'min:1'],
'commands.*.run_timeout_seconds' => ['nullable', 'integer', 'min:1'],
'commands.*.workflow_type' => ['nullable', 'string'],
'commands.*.delay_seconds' => ['nullable', 'integer', 'min:0'],
'commands.*.message' => ['nullable', 'string'],
'commands.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'],
'commands.*.payload_codec' => ['nullable', 'string'],
'commands.*.update_id' => ['nullable', 'string'],
'commands.*.exception_class' => ['nullable', 'string'],
'commands.*.exception_type' => ['nullable', 'string'],
'commands.*.exception' => ['nullable', 'array'],
'commands.*.failed_step_sequence' => ['nullable', 'integer', 'min:1'],
'commands.*.failed_activity_execution_id' => ['nullable', 'string', 'max:255'],
'commands.*.change_id' => ['nullable', 'string'],
'commands.*.version' => ['nullable', 'integer'],
'commands.*.min_supported' => ['nullable', 'integer'],
'commands.*.max_supported' => ['nullable', 'integer'],
'commands.*.attributes' => ['nullable', 'array'],
'commands.*.attribute_types' => ['nullable', 'array'],
'commands.*.attribute_types.*' => ['string'],
'commands.*.entries' => ['nullable', 'array'],
'commands.*.non_retryable' => ['nullable', 'boolean'],
'commands.*.parent_close_policy' => ['nullable', 'string'],
'commands.*.cancellation_policy' => ['nullable', 'string', 'in:try_cancel,wait_cancellation_completed,abandon'],
'commands.*.condition_key' => ['nullable', 'string'],
'commands.*.condition_definition_fingerprint' => ['nullable', 'string'],
'commands.*.condition_wait_occurrence_id' => ['nullable', 'string'],
'commands.*.signal_name' => ['nullable', 'string'],
'commands.*.timeout_seconds' => ['nullable', 'integer', 'min:0'],
'commands.*.endpoint_name' => ['nullable', 'string', 'max:191'],
'commands.*.service_name' => ['nullable', 'string', 'max:191'],
'commands.*.operation_name' => ['nullable', 'string', 'max:191'],
'commands.*.request_payload' => ['nullable'],
'commands.*.namespace' => ['nullable', 'string', 'max:191'],
'commands.*.caller_namespace' => ['nullable', 'string', 'max:191'],
'commands.*.service_call_id' => ['nullable', 'string', 'max:191'],
'commands.*.idempotency_key' => ['nullable', 'string', 'max:191'],
'commands.*.mode_override' => ['nullable', 'string', 'in:sync,async'],
'commands.*.wait_for' => ['nullable', 'string', 'in:accepted,completed'],
'commands.*.wait_timeout_seconds' => ['nullable', 'integer', 'min:0'],
'commands.*.target_workflow_instance_id' => ['nullable', 'string', 'max:191'],
'commands.*.target_workflow_run_id' => ['nullable', 'string', 'max:191'],
'commands.*.business_key' => ['nullable', 'string', 'max:191'],
'commands.*.labels' => ['nullable', 'array'],
'commands.*.memo' => ['nullable', 'array'],
'commands.*.search_attributes' => ['nullable', 'array'],
'commands.*.duplicate_start_policy' => ['nullable', 'string', 'in:reject_duplicate,return_existing_active'],
'commands.*.metadata' => ['nullable', 'array'],
'commands.*.request_payload_reference' => ['nullable', 'string', 'max:191'],
'commands.*.principal_subject' => ['prohibited'],
'commands.*.principal_method' => ['prohibited'],
'commands.*.principal_roles' => ['prohibited'],
'commands.*.principal_tenant' => ['prohibited'],
'commands.*.principal_claims' => ['prohibited'],
...WorkflowCommandNormalizer::parallelMetadataValidationRules(),
'commands.*.workflow_stream' => ['nullable', 'array'],
'commands.*.workflow_stream.operation' => ['required_with:commands.*.workflow_stream', 'string', 'in:append,close,error'],
'commands.*.workflow_stream.stream_name' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'],
'commands.*.workflow_stream.command_identity' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'],
'commands.*.workflow_stream.command_ordinal' => ['required_with:commands.*.workflow_stream', 'integer', 'min:0'],
'commands.*.workflow_stream.items' => ['nullable', 'array', 'max:'.WorkflowStreamService::DEFAULT_MAX_ITEMS_PER_APPEND],
'commands.*.workflow_stream.items.*' => ['array'],
'commands.*.workflow_stream.items.*.payload' => ['nullable'],
'commands.*.workflow_stream.items.*.payload_reference' => ['nullable', 'string', 'max:191'],
'commands.*.workflow_stream.items.*.payload_codec' => ['nullable', 'string', 'max:64'],
'commands.*.workflow_stream.items.*.idempotency_key' => ['nullable', 'string', 'max:191'],
'commands.*.workflow_stream.items.*.item_type' => ['nullable', 'string', 'max:64'],
'commands.*.workflow_stream.items.*.content_type' => ['nullable', 'string', 'max:191'],
'commands.*.workflow_stream.max_pending_items' => ['nullable', 'integer', 'min:1'],
'commands.*.workflow_stream.error_reason' => ['nullable', 'string', 'max:191'],
'commands.*.workflow_stream.retention_seconds' => ['nullable', 'integer', 'min:1'],
'sticky_cache' => ['nullable', 'array'],
'sticky_cache.worker_id' => ['required_with:sticky_cache', 'string', 'max:255'],
'sticky_cache.workflow_id' => ['required_with:sticky_cache', 'string', 'max:255'],
'sticky_cache.run_id' => ['required_with:sticky_cache', 'string', 'max:255'],
'sticky_cache.build_id' => ['required_with:sticky_cache', 'string', 'max:255'],
'sticky_cache.ttl_seconds' => ['required_with:sticky_cache', 'integer', 'min:1', 'max:3600'],
'sticky_cache.metrics' => ['nullable', 'array'],
'sticky_cache.metrics.hit' => ['nullable', 'integer', 'min:0'],
'sticky_cache.metrics.miss' => ['nullable', 'integer', 'min:0'],
'sticky_cache.metrics.eviction' => ['nullable', 'integer', 'min:0'],
'sticky_cache.metrics.forced_cold_replay' => ['nullable', 'integer', 'min:0'],
];
$completionRules = WorkflowTaskCompletionValidation::rules();

$topLevelRules = [];
if ($group) {
Expand Down
Loading
Loading