docs(design): record async disabled-execution + cross-app backstops
Update §4.3: the fire-time `enabled` re-check is shipped and test-backed on BOTH async arms (the unified outbox `!resolved.active` gate and the queue arm's fresh-script nack), with the specific regression tests cited, plus the same-app backstop now present on every async build path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -296,12 +296,23 @@ CLI builds bundle (manifests + script sources) for the subtree
|
|||||||
and it is **honored at match/schedule time** (`trigger_repo.rs`, `cron_scheduler.rs` — verified).
|
and it is **honored at match/schedule time** (`trigger_repo.rs`, `cron_scheduler.rs` — verified).
|
||||||
New work is `enabled` on **scripts** and **routes** only, plus runtime honoring in the matcher /
|
New work is `enabled` on **scripts** and **routes** only, plus runtime honoring in the matcher /
|
||||||
invoker.
|
invoker.
|
||||||
- **Outbox fire-time gap (verified):** the dispatcher does **not** re-check `enabled` on an
|
- **Fire-time re-check (shipped, test-backed):** the dispatcher re-checks `enabled` at fire time on
|
||||||
already-enqueued outbox row (`dispatcher.rs:699`), so disabling a trigger stops *new* matches while
|
every async path, so a pending event for a now-disabled trigger/script is dropped (not executed)
|
||||||
a pending item still fires. **Fix:** add an `enabled` re-check (trigger *and* script) at fire time
|
when it comes up — the §5.1 security-disable guarantee on the trigger path. **Outbox arm:**
|
||||||
in `resolve_trigger`, so a pending outbox row for a now-disabled trigger/script is dropped when it
|
`resolve_trigger` sets `active = trigger.enabled && script.enabled` and the async-HTTP/invoke
|
||||||
comes up — closing the gap cheaply, with no cache or kill-switch involvement. This is the home for
|
builders set `active = script.enabled`; `dispatch_one` drops the row via a single unified
|
||||||
the §5.1 security-disable guarantee on the trigger path.
|
`if !resolved.active` gate. **Queue arm:** `dispatch_one_queue` runs a separate path (it claims from
|
||||||
|
`queue_messages`, not the outbox), so in addition to `list_active_queue_consumers` filtering
|
||||||
|
`s.enabled`/`t.enabled` at list time, it re-checks the **freshly-read** `script.enabled` before
|
||||||
|
executing and **releases the claim (`nack`)** when disabled — closing the per-tick TOCTOU window
|
||||||
|
(a script disabled after the list snapshot but before its in-flight message runs). Both arms are
|
||||||
|
regression-locked: `dispatcher::tests::outbox_enabled_gate::disabled_http_outbox_row_is_dropped_without_executing`
|
||||||
|
covers the unified `!resolved.active` outbox gate, and
|
||||||
|
`dispatcher::tests::queue_enabled_gate::disabled_queue_consumer_releases_claim_without_executing`
|
||||||
|
covers the queue arm. **Same-app backstop:** every async build path (`resolve_trigger`,
|
||||||
|
`build_http_request`, `build_invoke_request`, `dispatch_one_queue`) rejects a row whose script's
|
||||||
|
`app_id` ≠ the row's `app_id`, so a hand-edited/restored row can't run one app's script under
|
||||||
|
another's `SdkCallCx` — the cross-app isolation boundary.
|
||||||
- **Provenance caveat (accepted):** a single boolean carries no "manual vs manifest" provenance, so a
|
- **Provenance caveat (accepted):** a single boolean carries no "manual vs manifest" provenance, so a
|
||||||
disabled entity looks the same however it got there — which is *why* the §4.2 conflict bit on the
|
disabled entity looks the same however it got there — which is *why* the §4.2 conflict bit on the
|
||||||
`enabled`/secrets subset exists, to stop the next apply silently reverting an operational disable.
|
`enabled`/secrets subset exists, to stop the next apply silently reverting an operational disable.
|
||||||
|
|||||||
Reference in New Issue
Block a user