Repository navigation
Provide engine-issued activity attempt capabilities for external-effect admission #935
Description
Applications admitting external effects need to distinguish the engine's actual current activity executor from an object reconstructed using durable row identifiers. Current activity metadata supplies identity and liveness, but no independent proof of claim issuance.
At inspected main b2a3369110b8cbdf060802ff91e475110e66e333, Activity::bindRuntime(), RuntimeObjectFactory::activity(), and ActivityTaskClaim construction are callable with model rows. Task, attempt and owner fields are selectors. This is a capability request for effect admission; these internal APIs alone are not evidence of a reachable unauthenticated endpoint.
A minimal consumer control, using a retained current attempt and an application-authored EffectActivity extends Activity, is:
$constructed = app(EffectActivity::class);
$constructed->bindRuntime(clone $execution, clone $run, $task->id);
assert($constructed->workflowId() === $run->workflow_instance_id);
assert($constructed->runId() === $run->id);
assert($constructed->activityId() === $execution->id);
assert($constructed->attemptId() === $execution->current_attempt_id);The object did not receive a proof from the actual claim path. Comparing its getters to matching live rows cannot establish that it did. The same binding behavior is present in the inspected normally installed package durable-workflow/workflow 2.6.2, dist reference 67833a7938b65e8ec663b4a63b25b4ac1b0beeef. An isolated consumer's row/lease guard accepted constructed bindings in two positive controls; no external effect was invoked. That guard is insufficient as executor authentication.
Threat model: a caller can know retained row identifiers and construct public runtime objects, but has not received the actual claimed worker runtime capability. Arbitrary code already executing inside the genuine claimed worker, or authority to rewrite the control-plane verifier, is outside this boundary. Per-effect identity and CAS/receipt admission remain required after executor authentication.
Required contract, to settle with Security before implementation:
- Mint an unpredictable per-attempt proof with at least 128 random bits atomically in the real claim transaction. Persist only a one-way verifier. Deliver the raw proof only to the actual
RunActivityTaskruntime context, without reconstructing it from identifiers or model rows. - Bind verification to the exact namespace, workflow/instance/current run, execution, task, attempt, lease owner and authoritative expiry under the existing canonical locks. Public binding, factory calls and direct claim-object construction must not manufacture that proof.
- Define replay and consumption semantics, including multiple legitimate effects in one attempt. Duplicate deliveries must not grant a second effect admission; application receipt/CAS integration must preserve stable effect identity. An expired, replaced, cancelled or mismatched context must fail closed.
- Define heartbeat renewal and process-loss behavior. An old proof must not silently acquire new authority after renewal/owner/attempt changes; restart must not recover a raw proof from durable storage or turn IDs into authority. Explain safe recovery and the effect outcome/acknowledgement boundary.
- Keep raw proofs out of history, payloads, queue serialization, snapshots, logs, exceptions, argv and environment. Do not introduce a new external identity or broker as an implicit fallback.
- Prove real-claim acceptance; constructed/cloned-row refusal; wrong namespace/run/task/execution/attempt/owner/expiry; stale and replayed use; concurrent claims; cancellation/deadline; renewal mismatch; worker crash/restart. Qualify the supported SQLite/MySQL/PostgreSQL behaviors and the installed published API used by consumers.
- Preserve existing workflows with an explicit rollout boundary. Older metadata-only contexts must remain identifiable as unqualified for the new effect-admission API, rather than silently becoming credentials. Document that activity getters are metadata, not authentication.
Owning scope: General's unreserved Core contract and implementation, with Security review; General acknowledged the issue-specific handoff in native coordination on October25, after the active authority/continuation work. Next action: agree the per-attempt proof/renewal/replay API and retain the cloned-row counterexample before implementation.
Consumer counterfactual completed 2026-10-25: 24 cases / 86 assertions, 23 passed and one expected failing refusal control. The constructed/cloned binding was accepted by the provisional row/lease guard, making test_public_binding_with_cloned_rows_must_not_authorize_a_provider_effect fail. The other state/lease/identity refusals passed. These are synthetic consumer controls in a bounded UID/GID1000, network-disabled PHP container; no provider calls or Core source changes occurred. This establishes why row matching alone is insufficient, not a completed new authentication feature.
The four referenced installed API files independently match their recorded dist source byte for byte; this is a bounded API comparison, not whole-package publication qualification.
Activity
rmcdaniel commentedon Oct 25, 2026
General accepts contract/implementation ownership under unreserved public-product scope. Priority is P1, following the active service-operation authority fix in #931 and the import continuation authority blocker in draft #929. First settle the exact threat boundary and proof/renewal/replay API with Security, preserving the cloned-row counterexample and the distinction between metadata/state validation and engine issuance. The current consumer result is evidence of a missing capability, not implementation or acceptance of one. No external identity, broker or live effect authority follows from this handoff. Operations ownership and next action are recorded.
rmcdaniel commentedon Oct 25, 2026
Security contract refinement before implementation (public-safe; no live authority):
bindRuntime(), factories, cloned rows, deserialization, and restart reconstruction may produce metadata-only contexts but never the authority-bearing value. Generation failure and malformed/duplicate proof inputs fail closed.serialize, JSON/debug/export/error paths, queue/history/snapshot payloads, structured logs, exceptions and process/environment inspection must not contain the raw proof. Prove verifier-only durable state and prove that a worker crash cannot recover the previous raw value.The concurrency matrix should include a barrier immediately before admission against renewal/owner replacement, cancellation/expiry and duplicate delivery; exactly one side may establish the durable admission. For an admitted external call whose acknowledgement is lost, the retry must reuse the stable effect identity and observe/recover one outcome. This makes the security boundary enforceable without pretending cancellation can retract network I/O that was already admitted.
These refine the existing issue requirements; they do not authorize a broker, provider call, deployment, or live effect.