You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Psalm\\LaravelPlugin\\Handlers\\Ai\\LlmPromptSinkHandler subscribing to AfterMethodCallAnalysisEvent / AfterFunctionCallAnalysisEvent.
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.
Property reads keep the existing LlmOutputTaintHandler (already handler-driven for \$response->text).
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.
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-sourceannotations.Every redeclaration triggers CLAUDE.local.md's documented footgun: Psalm's
ClassLikeNodeScannerresetsclass_implements/parent_interfacesand re-populates from the stub's own clauses. The Round-2 fix in #937 mitigates this by copying fullimplements/extends/useclauses plus blocking direct methods, but the cost is:getTimeout,withModelFailover,assertPrompted,fake, …) implicitly inherited from the reflected source — works today, but quietly breaks if the next release inlines them on the class.tests/Application/laravel-test.sh(composer require laravel/aistep added in Add taint analysis stubs forlaravel/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:
Psalm\\LaravelPlugin\\Handlers\\Ai\\LlmPromptSinkHandlersubscribing toAfterMethodCallAnalysisEvent/AfterFunctionCallAnalysisEvent.Handlers\\Validation\\ValidationTaintHandler's sink list — mapsLaravel\\Ai\\Contracts\\Agent::prompt#1 → llm_prompt,Laravel\\Ai\\Tools\\SimilaritySearch::withDescription#1 → llm_prompt, etc.LlmOutputTaintHandler(already handler-driven for\$response->text).Tool::description(),Agent::instructions()— the inert@psalm-taint-sink llm_prompt returnthat we dropped in Add taint analysis stubs forlaravel/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
Tool::description() / Agent::instructions()gap.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 excepthelpers.phpstub(taint sink on a free function — already minimal).Out of scope (separate follow-ups)
Tool::description()return) — needs a different handler shape.prism-php-prism,openai-php-client,anthropic-ai-sdk,neuron-core-neuron-ai,llphant-llphant.cc #484