Skip to content

fix: make generic content-publishing tools opt-in for pipeline AI steps#2853

Merged
chubes4 merged 1 commit into
mainfrom
fix-pipeline-publish-opt-in
Jul 5, 2026
Merged

fix: make generic content-publishing tools opt-in for pipeline AI steps#2853
chubes4 merged 1 commit into
mainfrom
fix-pipeline-publish-opt-in

Conversation

@chubes4

@chubes4 chubes4 commented Jul 5, 2026

Copy link
Copy Markdown
Member

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 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 (AI-improvised "rejection reports"/"admin logs") that bypassed the intended upsert_event handler and its junk-title guard.

The default posture was opt-out, not opt-in, via four compounding code paths in inc/:

  1. PublishWordPressAbility / UpsertPostAbility / InsertContentAbility register generic content-writing abilities (free-form post_type, post_status: publish) exposed to pipeline mode.
  2. AbilityToolSource::matchesModes() (~L259) and DataMachineToolRegistrySource expose ANY tool whose declared modes include pipeline to EVERY pipeline AI step, regardless of the flow's adjacent handler.
  3. ToolManager defaults a mode-less tool to MODE_PIPELINE (~L238, L373).
  4. ToolPolicyResolver::resolve() only enforced an allow-list when one was explicitly set (allow_only_explicit, ~L137). A step with no enabled_tools fell 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:

  • its adjacent-handler tools (the flow's declared publish/upsert handler, e.g. wordpress_publish / upsert_event),
  • research/read tools (search, post-read, link audit, etc.),
  • disposition tools (reject_source, defer_item).

Generic publish/write tools appear only when the step explicitly lists them in enabled_tools (or an allow-mode tool_policy).

Mechanism (principled and layer-pure — not a slug blocklist)

  • A new declared tool flag requires_pipeline_opt_in, set at each writing tool's own registration site (UpsertPostAbility, InsertContentAbility), exactly mirroring how requires_opt_in / requires_config / modes are declared. ToolManager::resolveToolDefinition() now propagates this flag from the _callable wrapper into the resolved definition.
  • Ability tools projected from the canonical datamachine-publishing category are auto-gated (ToolPolicyResolver::isPipelineWriteOptInTool()). ability_category is already stamped onto ability-projection tools by AbilityToolAdapter. This covers publish-wordpress / update-wordpress / send-email defensively if they are ever projected as tools.
  • ToolPolicyResolver enforces 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-content mixes 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_publish handler tool — that is mandatory flow plumbing (DataMachineMandatoryToolPolicy::isMandatory(): has handler, no ability), produced by AdjacentHandlerToolSource via the _handler_callable path in WordPress.php. The gate exempts mandatory tools.

Proven by tests/Unit/Engine/AI/Tools/PipelinePublishOptInTest.php (see test_pipeline_keeps_adjacent_publish_handler_tool and test_pipeline_excludes_write_tools_but_keeps_adjacent_and_disposition_tools).

Exact behavior change

Surface Before After
Pipeline AI step, no enabled_tools generic publish/write tools ambient only adjacent-handler + read + disposition tools
Pipeline AI step, enabled_tools: ['upsert_post'] upsert_post available upsert_post available (opt-in works)
Pipeline AI step whose next step IS a wordpress_publish publish step wordpress_publish tool available wordpress_publish tool still available (flow plumbing)
Chat / system / editor generic tools available unchanged

Excluded by default in pipeline mode: upsert-post, insert-content, and any datamachine-publishing-category ability projection (e.g. a future publish-wordpress projection).
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/step tool_policy.

Tests

Added tests/Unit/Engine/AI/Tools/PipelinePublishOptInTest.php covering:

  1. Pipeline step with adjacent upsert_event + fetch disposition tools → excludes the flagged/category write tools, includes upsert_event, reject_source, defer_item (requirement 1).
  2. Pipeline step whose next step is a publish handler → the adjacent publish handler tool is preserved; generic publish still excluded (requirement 2).
  3. Pipeline step that explicitly opts in via enabled_tools / allow-mode tool_policy → gated tool is restored (requirement 3).
  4. Category-based gating (datamachine-publishing) excluded by default, restored on opt-in.
  5. Chat mode is not gated (non-regression).

The new gate logic was also exercised end-to-end against the real ToolPolicyResolver class via reflection (standalone harness), confirming all 18 identification/filtering cases pass.

Note: composer test runs through homeboy test, which provisions the PHPUnit/wp-phpunit sandbox via wp-codebox; in this shell the wp-codebox binary-detection was broken so the full suite could not be executed locally. The new tests follow the exact patterns of the existing ToolPolicyResolverTest / HandlerToolResolutionTest and will run in CI.

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.

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.
@homeboy-ci

homeboy-ci Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Homeboy Results — data-machine

Lint

review lint — passed

ℹ️ Full options: homeboy self docs commands/lint
Deep dive: homeboy review lint data-machine --changed-since 77d6380

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-review-lint-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-review-lint-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28746610577

Test

review test — failed

ℹ️ No tests ran — the runner failed before producing results. See raw_output.stderr_tail / raw_output.stdout_tail for the underlying error (bootstrap failure, missing deps, DB connection, etc.).
ℹ️ To run specific tests: homeboy test data-machine -- --filter=TestName
ℹ️ Auto-fix lint issues: homeboy refactor data-machine --from lint --write
ℹ️ Collect coverage: homeboy test data-machine --coverage
ℹ️ Analyze failures: homeboy test data-machine --analyze
ℹ️ Pass args to test runner: homeboy test -- [args]
ℹ️ Full options: homeboy self docs commands/test
Deep dive: homeboy review test data-machine --changed-since 77d6380

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-review-test-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-review-test-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28746610577

Audit

review audit — passed

Deep dive: homeboy review audit data-machine --changed-since 77d6380

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-review-audit-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-review-audit-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/28746610577
Tooling versions
  • Homeboy CLI: homeboy 0.281.8+15d2280a58ed+cd7d2344
  • Extension: wordpress from https://github.com/Extra-Chill/homeboy-extensions
  • Extension revision: 68d84557
  • Action: unknown@unknown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pipeline AI steps get generic publish-wordpress/upsert-post tools by default (opt-out, not opt-in) → silent junk-post leakage

1 participant