Pipeline AI steps were offered generic content-publishing ability tools
(publish-wordpress, upsert-post, insert-content) by default, with no
allow-list required. A model could create arbitrary post types with
post_status=publish even when the flow's declared handler was something
else (e.g. upsert_event), bypassing the handler's guards entirely. On
the events site this leaked 224 junk post-type items.
The default posture was opt-out, not opt-in, via four compounding code
paths: generic publish abilities registered with pipeline mode; the
static-registry source exposed any pipeline-mode tool to every step;
mode-less tools defaulted to pipeline; and the policy resolver only
enforced an allow-list when one was explicitly set.
This makes generic content-writing tools opt-in for pipeline AI steps.
A step now receives by default only its adjacent-handler tools,
research/read tools, and disposition tools. Generic publish/write tools
appear only when the step explicitly lists them in enabled_tools or an
allow-mode tool policy.
Mechanism (principled, not a slug blocklist):
- A new declared tool flag 'requires_pipeline_opt_in' (set at each
writing tool's registration site, like requires_opt_in/requires_config).
- Ability tools projected from the canonical 'datamachine-publishing'
category are auto-gated.
- ToolPolicyResolver enforces the gate in pipeline mode only, after the
generic policy pass, preserving mandatory adjacent-handler plumbing
(wordpress_publish, upsert_event, reject_source, defer_item, ...) and
honoring explicit opt-in.
- Chat/system/editor modes are unaffected.
Adjacent-handler publishing is preserved: a flow whose next step IS a
WordPress publish step still gets its wordpress_publish handler tool
(mandatory flow plumbing), proven by the new test suite.
Fixes #2852.
Fixes #2852
Root cause
Pipeline AI steps were offered generic content-publishing ability tools (
publish-wordpress,upsert-post,insert-content) by default, with no allow-list required. A model could create arbitrary post types withpost_status=publisheven when the flow's declared handler was something else (e.g.upsert_event), bypassing the handler's guards entirely. On the events site this leaked 224 junkpost-type items (AI-improvised "rejection reports"/"admin logs") that bypassed the intendedupsert_eventhandler and its junk-title guard.The default posture was opt-out, not opt-in, via four compounding code paths in
inc/:PublishWordPressAbility/UpsertPostAbility/InsertContentAbilityregister generic content-writing abilities (free-formpost_type,post_status: publish) exposed to pipeline mode.AbilityToolSource::matchesModes()(~L259) andDataMachineToolRegistrySourceexpose ANY tool whose declaredmodesincludepipelineto EVERY pipeline AI step, regardless of the flow's adjacent handler.ToolManagerdefaults a mode-less tool toMODE_PIPELINE(~L238, L373).ToolPolicyResolver::resolve()only enforced an allow-list when one was explicitly set (allow_only_explicit, ~L137). A step with noenabled_toolsfell through to the full preset.Net: generic publish was ambient availability for every pipeline step.
What changed and why
Made generic content-writing abilities opt-in for pipeline AI steps. A step now receives by default only:
wordpress_publish/upsert_event),reject_source,defer_item).Generic publish/write tools appear only when the step explicitly lists them in
enabled_tools(or an allow-modetool_policy).Mechanism (principled and layer-pure — not a slug blocklist)
requires_pipeline_opt_in, set at each writing tool's own registration site (UpsertPostAbility,InsertContentAbility), exactly mirroring howrequires_opt_in/requires_config/modesare declared.ToolManager::resolveToolDefinition()now propagates this flag from the_callablewrapper into the resolved definition.datamachine-publishingcategory are auto-gated (ToolPolicyResolver::isPipelineWriteOptInTool()).ability_categoryis already stamped onto ability-projection tools byAbilityToolAdapter. This coverspublish-wordpress/update-wordpress/ send-email defensively if they are ever projected as tools.ToolPolicyResolverenforces the gate in pipeline mode only, after the generic policy pass, preserving mandatory adjacent-handler plumbing and honoring explicit opt-in. Chat / system / editor modes are unaffected.This was chosen over (a) a category-only rule, because
datamachine-contentmixes read + write tools and gating it wholesale would regress read tools; and (b) a slug blocklist in the policy layer, which would violate layer purity and the "add a new vendor/tool = config change, not a code change in the generic layer" rule. The generic policy layer reads a declared posture; each writing tool owns its own flag.Adjacent-handler publishing is preserved (no regression)
A flow whose next step IS a WordPress publish step still gets its
wordpress_publishhandler tool — that is mandatory flow plumbing (DataMachineMandatoryToolPolicy::isMandatory(): hashandler, noability), produced byAdjacentHandlerToolSourcevia the_handler_callablepath inWordPress.php. The gate exempts mandatory tools.Proven by
tests/Unit/Engine/AI/Tools/PipelinePublishOptInTest.php(seetest_pipeline_keeps_adjacent_publish_handler_toolandtest_pipeline_excludes_write_tools_but_keeps_adjacent_and_disposition_tools).Exact behavior change
enabled_toolsenabled_tools: ['upsert_post']wordpress_publishpublish stepwordpress_publishtool availablewordpress_publishtool still available (flow plumbing)Excluded by default in pipeline mode:
upsert-post,insert-content, and anydatamachine-publishing-category ability projection (e.g. a futurepublish-wordpressprojection).Still available by default: adjacent-handler tools (
wordpress_publish,upsert_event, …), disposition tools (reject_source,defer_item), research/read tools (web_fetch, search, post readers, link audit).How to opt back in: list the tool in the step's
enabled_tools, or use an allow-mode agent/steptool_policy.Tests
Added
tests/Unit/Engine/AI/Tools/PipelinePublishOptInTest.phpcovering:upsert_event+ fetch disposition tools → excludes the flagged/category write tools, includesupsert_event,reject_source,defer_item(requirement 1).enabled_tools/ allow-modetool_policy→ gated tool is restored (requirement 3).datamachine-publishing) excluded by default, restored on opt-in.The new gate logic was also exercised end-to-end against the real
ToolPolicyResolverclass via reflection (standalone harness), confirming all 18 identification/filtering cases pass.Related
The events-layer mitigation (the junk-title guard and the leaked-items cleanup for the events site) is tracked in Extra-Chill/data-machine-events#412, a separate parallel PR. This core PR is the durable fix.