Skip to content

Provide engine-issued activity attempt capabilities for external-effect admission #935

Open

Description

@rmcdaniel

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 RunActivityTask runtime 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

  1. added
    kind:featureA public product capability or experience is requested
    priority:P2Normal-priority product work
    on Oct 25, 2026
  2. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    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.

  3. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    Security contract refinement before implementation (public-safe; no live authority):

    1. Treat the capability as proof of one exact claim epoch, not merely one attempt. Any owner change, re-claim, attempt replacement, or authority-bearing lease renewal must atomically invalidate the previous verifier. The preferred renewal contract is rotate-and-return a new in-memory capability to the currently executing claim; an already-issued proof must never gain a later expiry because a database row was renewed. If renewal cannot safely deliver a replacement, keep the original proof expiry and require a fresh claim path rather than extending it implicitly.
    2. Make the raw proof an opaque in-memory value with no public constructor from identifiers, no string conversion, and explicit refusal of serialization/JSON/history/payload export. Public 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.
    3. Define one transaction as the effect-admission linearization point: under the canonical attempt/claim/lease/cancellation locks, verify the current claim epoch and capability, then reserve or recover the deterministic effect receipt/idempotency identity. Commit that admission before performing network I/O; do not hold database locks across an external call. Cancellation or expiry before this reservation refuses. State changes after a committed reservation do not erase an already-admitted effect; retries recover the same receipt and provider idempotency identity rather than authorizing a second effect.
    4. Verification must be constant-time over a domain-separated verifier and bind the exact namespace, workflow/instance/run, execution, task, attempt, claim epoch, owner and audience/purpose. A proof valid for one effect-admission API must not become a general runtime credential. Consumer-supplied effect identity and authorization remain independently checked; a genuine worker capability is necessary, not sufficient.
    5. Add disclosure controls as executable tests: 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.

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:featureA public product capability or experience is requestedpriority:P2Normal-priority product work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions