Skip to content

laravel/ai: migrate taint coverage from class-redeclaration stubs to handler-based table #938

@alies-dev

Description

@alies-dev

Follow-up to #937 / #484.

Problem

stubs/integrations/laravel-ai/ currently redeclares ~10 classes/traits/interfaces (AnonymousAgent, TextResponse, AgentResponse, StreamableAgentResponse, Tools/Request, Tools/SimilaritySearch, Promptable, Files/Document, Files/Image, …) purely to attach @psalm-taint-sink/@psalm-taint-source annotations.

Every redeclaration triggers CLAUDE.local.md's documented footgun: Psalm's ClassLikeNodeScanner resets class_implements / parent_interfaces and re-populates from the stub's own clauses. The Round-2 fix in #937 mitigates this by copying full implements/extends/use clauses plus blocking direct methods, but the cost is:

  • Stub bodies drift on every laravel/ai release (signatures, default values, new properties, renamed methods).
  • The Round-2 hybrid leaves provider-internal methods (getTimeout, withModelFailover, assertPrompted, fake, …) implicitly inherited from the reflected source — works today, but quietly breaks if the next release inlines them on the class.
  • Drift detection currently depends on the application-test pass in tests/Application/laravel-test.sh (composer require laravel/ai step added in Add taint analysis stubs for laravel/ai (LLM prompt injection) #937). That catches signature breaks at analyzer-time but only on the CI matrix that runs the integration job.

Proposed approach ("path c")

Drop the redeclarations entirely. Move every annotation into a handler-driven table:

  1. Psalm\\LaravelPlugin\\Handlers\\Ai\\LlmPromptSinkHandler subscribing to AfterMethodCallAnalysisEvent / AfterFunctionCallAnalysisEvent.
  2. A static FQN+param-name table — same shape as Handlers\\Validation\\ValidationTaintHandler's sink list — maps Laravel\\Ai\\Contracts\\Agent::prompt#1 → llm_prompt, Laravel\\Ai\\Tools\\SimilaritySearch::withDescription#1 → llm_prompt, etc.
  3. Property reads keep the existing LlmOutputTaintHandler (already handler-driven for \$response->text).
  4. Return-value sinks (Tool::description(), Agent::instructions() — the inert @psalm-taint-sink llm_prompt return that we dropped in Add taint analysis stubs for laravel/ai (LLM prompt injection) #937) become expressible: the handler sees the called method, looks up its FQN in the table, and routes the return type into a synthetic taint sink.

Benefits

  • Zero class redeclarations → no stripped metadata, no verbatim copy obligation per CLAUDE.local.md.
  • Drift cost evaporates: the only thing that breaks on a laravel/ai release is a missing/renamed FQN entry in the table — fast to detect with a unit test asserting every entry resolves to a real method.
  • Return-sink coverage lands at the same time, closing the Tool::description() / Agent::instructions() gap.
  • Same registration gate as today (isInstalledAndSatisfies('laravel/ai', '^0.6')).

Estimated scope

~3h. Mostly: handler + table + unit test that walks the table against vendor/laravel/ai/ via reflection. Stubs deleted except helpers.phpstub (taint sink on a free function — already minimal).

Out of scope (separate follow-ups)

  • Tool poisoning detector (non-constant Tool::description() return) — needs a different handler shape.
  • Lethal Trifecta architectural issue.
  • Extending the same table-based approach to prism-php-prism, openai-php-client, anthropic-ai-sdk, neuron-core-neuron-ai, llphant-llphant.

cc #484

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions