Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Pandora Laravel Package

michal78/laravel-pandora

An agentic framework for Laravel: build agents, tools, approvals, automations and memory under explicit, auditable policy, with a Livewire control center for operating them.

View on GitHub
Deep Wiki
Context7
v0.1.3

This release carries two security fixes. Untrusted content could close its own delimiter and continue outside it in a system message — a memory is the one that matters, because it persists — and read_config could publish an allowlisted credential-shaped key into a tool result. Both are detailed below. Hosts should upgrade; no host action is required beyond doing so.

Phase 9's threat audit continues. First T1, T4, T10 and T11, chosen because all four sit where a fake stands in for a real boundary; then T12 and T14, chosen because a concurrency test that never actually contends passes trivially and nothing about reading it says so; then T2, expected to be the dull one. T10 and most of T12 audited clean, which is a result rather than an absence of one.

The T12/T14 findings are all in the suite rather than in shipped behaviour — no security fix was needed — and the root cause of four of the five is that the suite runs serially, so every row lock in the codebase was untestable and therefore untested.

T2's are the same: no shipped behaviour was wrong, and both findings are about what the suite could not see. Tenancy has more tests than any other threat in the model and all three of its controls held under removal — but nothing checked that any given model had opted into the scope, and nothing checked that a queued job re-enters the tenant it carries. The second one shares a root cause with T12/T14: jobs run inline under QUEUE_CONNECTION=sync, so the dispatching process hands a job the context a real worker would have had to re-establish for itself.

Security

  • Untrusted content could close its own delimiter. <file>, <memory> and <environment> interpolated their content straight between the markers, so a document, a remembered note or an agent's name containing </file> — or </memory>, or </environment> — closed the region and continued outside it, in a system message. A delimiter the content can write is not a delimiter.

    Memory is the one that matters, because it persists. A memory is written by the remember tool, which is driven by model output, which may be reading an attacker's page: store one crafted note and every later run in that scope carried it, in the instruction region, past a marker it had closed. UntrustedBlock now neutralises every closing marker for its own tag, case- and whitespace-insensitively, and neutralises rather than strips, so a style guide containing </file> in a code sample still reads correctly.

    Behaviour change: attached context files and retrieved memories now reach the model as user messages rather than system ones. Both are UNTRUSTED in the trust boundary — the same line as a web page and a channel message — and every other untrusted string in the system is already kept out of the instruction position. No host action is needed; if you depend on the exact prompt layout, the section order is unchanged.

  • read_config refuses credential-shaped keys even when allowlisted. The allowlist is exact-match and never a prefix, which was already the right design, but an exact allowlist is still a person's judgement and services.stripe.secret is a key somebody could reasonably add while wiring up a tool. One config line and a live credential became a tool result, read by a model and stored in an execution row the redactor would not clean, because the key it is filed under is value. Refused on the key, before the value is read. The refusal names the config entry to remove.

    The denylist is a hard-coded baseline plus whatever the deployment adds, not the redaction list alone. pandora.security.redact_keys is tuned for output noise — an operator dropping session because it clutters every trace is making a reasonable call about logs — and deriving the refusal from it meant a change made for one purpose silently weakened another. Narrowing redact_keys now narrows redaction and cannot touch this.

Fixed

  • pandora:tool:list no longer prints required for tools that need no approval. RiskLevel::requiresApprovalByDefault() hard-coded high || critical while the gatekeeper reads pandora.approvals.required_for. Both agreed on the shipped defaults, so a deployment that narrowed the floor to critical got a console table saying a high-risk tool required a human when it did not. Found by deleting a mitigation and watching nothing fail — the method was never on the enforcement path.

Changed

  • The MySQL leg of the matrix runs in a fraction of the time. It took 604 seconds against MariaDB's 232 on identical tests, and the cause was the test harness meeting MySQL 8's DDL path. With no wrapping transaction available — Pandora catches unique-key violations as normal control flow, and on PostgreSQL a failed statement poisons the enclosing transaction — the harness empties every table between tests: ~35 tables times ~1,830 tests, or roughly 64,000 statements per run.

    TRUNCATE is DDL. InnoDB drops and recreates the tablespace, MySQL 8 routes that through its transactional data dictionary, and it fsyncs, because MySQL 8 ships the binary log on where MariaDB ships it off (verified: mysql:8.4 reports log_bin=1, mariadb:11 reports 0). The cleanup is now DELETE, which is ordinary DML — no tablespace churn, no dictionary write, no implicit commit — and it is faster on every engine rather than only the slow one.

    Measured on a real MySQL 8.4: the same 260 tests went from 395.77s to 31.34s.

    DELETE does not reset AUTO_INCREMENT, and nothing here depends on that — Pandora's keys are ULIDs, and no test asserts an id value against the two auto-increment tables in play.

  • The table listing is cached for the life of the process. Schema::getTables() ran once per test, which on MySQL 8 is a data-dictionary query, to answer a question whose answer changes only when the migrations do. Invalidated in the one place the schema is rebuilt.

Added

  • Security/ExactlyOnceUnderLockTest (T14) — the two row locks that carry "exactly once", which nothing had ever exercised. Deleting lockForUpdate() from ApprovalManager::resolve() left all 1,809 tests green, and deleting it from ExecuteToolCall::fanIn() did too: a lock does nothing in a serial test and cannot fail one. The new tests open a second connection to the same database and hold a real row lock on it. They skip on SQLite, where lockForUpdate() compiles to nothing because SQLite has no row locking, and run on the four server-engine legs of the matrix.
  • Two idempotency tests that now dispatch what a queue would dispatch (T14). The retry cases sent ExecuteToolCall without the tenant and actor a real at-least-once redelivery carries, so the actor came back null and the tool's own authorize() refused the second call — they were proving that a job stripped of its actor is denied. Every guard in ExecuteToolCall could be deleted and they stayed green; dispatched faithfully, the same removal refunds the customer twice. A further case redelivers while the run is still waiting on another call, which is the only scenario where the execution-row guard stands alone rather than being covered by the terminal-run check beside it.
  • A test for the stale Approve button on a cancelled run (T14). ResumeApprovedRun refuses to act on a terminal run, and nothing reached that check.
  • Two tests for what counts as a replay (T12). Replay protection is a unique INSERT, so both WebhookReceiver and AutomationDispatcher catch a constraint violation as a normal outcome, and DetectsUniqueViolations keeps that catch narrow. Widening either to every QueryException left all 28 webhook and idempotency tests green — they all reach a healthy database — while in production it would answer this webhook was already processed to a delivery that was in fact dropped. The insert now fails for a reason that is not uniqueness, and the fault has to come back out as a fault.
  • Security/TenantScopeCoverageTest (T2) — the tenant scope's opt-in, which was a convention rather than a control. Removing use BelongsToTenant; from each of the 26 models that carry it, one at a time, found six that left the whole suite green: Approval, AuditLog, Observation, WebhookDelivery, ChannelDelivery and ChannelLinkCode — the first two being the record of who authorised a destructive tool call and the record that it happened. It hid because the write side never depended on the trait (ApprovalManager::request() and AuditLogger::record() set tenant_id explicitly) and because no test ever performed a cross-tenant read of those models — a deleted scope is invisible to a same-tenant read. The new test asks the migrated schema which tables carry a tenant_id column and requires the trait on every model mapping to one, so the model this is really about — the one added next month — cannot omit it. ProviderCredential is the single exemption, pinned by a test of its own.
  • Security/QueuedJobTenancyTest (T2) — the tenant a queued job carries. ResolvesPandoraContext re-enters it, with a docblock naming the stake: "Forgetting this is the classic way a queued job silently reads across every tenant." Removing the re-entry left all 1,818 tests passing, twice — once by nulling the carried tenant, once by dropping the wrapper so the job inherits the worker's context. Under sync the ambient tenant stands in for the carried one; and losing a tenant does not make a read fail, it makes it wider, so the job finds its run and succeeds and every "it worked" assertion passes. The new tests dispatch from outside any tenant, the way a worker actually starts, and assert the leak: an audit row must carry the tenant the job was handed, and a job given another tenant's run id must leave that run in queued.
  • Two tests naming Approval and AuditLog in TenantIsolationTest (T2). The two objects with real authority deserve an assertion a reader can find by name; the approval case asserts unreachability by id through ApprovalManager::approve(), not merely absence from a list.
  • Security/UntrustedContextTest, InjectionToDestructiveCallTest, ApprovalArgumentFidelityTest, ApprovalFloorAgreementTest (T1). The end-to-end one is the sentence the threat model actually makes and nothing had ever exercised: a model reads a poisoned document and then demands a refund, in one run, through the real loop. The fake provider obeys the injection completely, on purpose — the mitigation has to be the layers, not the model's judgement.
  • Security/CredentialExtractionTest (T4) — the extraction clause, which had no test.
  • A broadcast test that checks what its name claims (T11). BroadcastTest had one called "versions and redacts every broadcast payload" that asserted nothing about redaction and passed with the redactor deleted from the base class. Renamed to what it does, with the promised assertion added beside it.
v0.1.2

Phase 9's threat audit begins. Four of the fifteen threats now have tests that were checked by removing the mitigation and watching them go red — and the first four found one live vulnerability, one criterion that described behaviour the package does not have, and one rule that had never been enforced anywhere.

Security

  • The MCP HTTP client no longer follows redirects (SSRF). Guzzle follows them by default, so a hostile or compromised MCP server could answer 302 Location: http://169.254.169.254/latest/meta-data/ and have Pandora re-send its POST to the cloud metadata endpoint — across an HTTPS-to-HTTP downgrade, into the link-local range — then return the response body to the model as tool output. An MCP endpoint is operator-configured precisely so that nothing on the far side chooses where this application connects; a Location header was a way around that. Redirects are now disabled at the client and a 3xx is refused with a new redirected reason rather than reported as server_unavailable, which would have read to an operator as an outage.

    No action needed on upgrade. A legitimate MCP server does not redirect its JSON-RPC endpoint; if one in your deployment does, point endpoint at the final URL. The refusal names the location it declined to follow.

    Hidden until now because every MCP test binds FakeMcpServer in place of the HTTP transport — a fake that never had a URL cannot lose one. Mcp/TransportUrlOriginTest drives the real transport against Http::fake() and asserts the URL requested, not merely the answer received.

Added

  • Architecture/NoOutboundHttpFromToolsTest (T6a). No core tool makes an outbound request, and now nothing can add one quietly: the source, the stream wrappers and the constructor wiring of every shipped tool are all checked. The threat model's SSRF allowlist is documented as a specification for a tool that does not exist rather than a control that is running, and this test is what goes red on the day it needs to be built.
  • Skills/UntrustedSkillTest (T9). A hostile skill body — install instructions, a shell command, an embedded tool call, a prompt-injection payload — is stored verbatim, lands disabled, grants nothing by declaring required_tools, and executes nowhere.
  • Architecture/ModuleBoundaryTest asserts T15 over every model. No $guarded = [], an explicit $fillable on all twenty-nine, and no unserialize() anywhere in src/.

Fixed

  • ADR-0008's last consequence was wrong, and is amended. It said skill instructions still reach the prompt. Nothing in src/ reads them: a skill can be imported, attached to an agent and shown on its detail page, and its text reaches no prompt because nothing composes it into one. Skills are inert, not merely unprivileged. The test asserts the current state, so wiring them into context turns it red and the untrusted-content handling gets built at the same time rather than assumed.
v0.1.1

Installing v0.1.0 from Packagist into a fresh Laravel application — which nobody had done, because every test in the suite runs against the source tree through Testbench — found the package's most visual feature silently absent on a stock install.

Fixed

  • pandora:status no longer calls the control center "enabled" when it cannot run. pandora.ui.enabled says the control center is wanted; Livewire says it can exist. The service provider returns early and registers no /pandora route at all when the class is missing, but the status line read only the config flag. Following the documented path on a stock install therefore produced a 404 on the thing the README leads with. Three states now — headless, unavailable, enabled.
  • pandora:install names Livewire as its own step, instead of closing with "Then open /pandora" and never mentioning it.
  • The README says plainly that the bare install is headless, and what to add for the UI.

Changed

  • CI retries composer update three times. A single curl error 60 fetching one zipball turned the whole matrix red with nothing wrong in the package. Cosmetic before; not any more, now that a green matrix is a required check on a branch where merging publishes.
  • The matrix reports through one check, tests complete. A branch ruleset names the checks it requires and a matrix expands into one check per leg, so requiring them individually means editing the ruleset every time a database is added — and a leg added but not required is a leg nobody has to pass. It uses an explicit result check rather than bare needs:, because a skipped or cancelled job does not fail its dependents.

Documentation

  • docs/development/fake-boundaries.md gains a sixth entry: the suite's own dependencies. livewire/livewire is a dev dependency, so class_exists() is true for every test that will ever run and the code handling its absence is unreachable by construction — untestable by layout rather than untested by oversight. It is the same lesson as the two Phase 6 defects, in a place nobody had thought to look, and it was only findable by installing the published package.
v0.1.0

The first published release. Nine phases of work: a durable agent kernel, tools with approvals, memory, automations, delegation, an MCP client and server, workspaces on object storage, channels with a Slack reference extension, and a Livewire control center — 1,756 tests, PHPStan level 8 with no baseline, Pint clean.

It is 0.x deliberately. The public API is usable and in use; it is not yet promised. Phase 9 — which proves every T1–T15 threat by removing its mitigation and watching the test fail — stands at 3 of 34 criteria. docs/product/support-statement.md names what is supported, what is excluded by design, and what ships known untested. Read it before depending on this in production.

Everything below this heading is the development history that produced it, kept by phase.

Known untested at 0.1.0

  • Two channel identities interleaving on one account in real time (Phase 8 §5). The sequential case is driven against a real Slack workspace and holds; the concurrent one needed a second Slack account that did not exist. A defect here would be a race in session resolution, not a wrong isolation key.
  • T1–T15 are not yet proved by mitigation-removal. Each threat names passing tests, and those tests have not all been audited against the bar Phase 9 sets for them.
  • Retrieval quality with the default embedding provider. HashEmbeddingProvider is lexical, not semantic; the vector path is real, the semantics are not. Configure a real provider for recall.

See docs/development/fake-boundaries.md for the full inventory of what the test suite's fakes structurally cannot prove.

Phase

An agent that remembers is an agent that will repeat something, so the security question changes from "may this actor do this?" to "whose was this, and who is standing here now?" — asked at retrieval, about a fact written by someone no longer in the room.

Added

  • MemoryItem with six scopes (global, tenant, user, agent, conversation, workspace), six types, provenance, confidence, sensitivity and expiry.
  • Scoped retrieval. Scope is derived from the run's session and can never be named by a tool argument. The recall tool has one parameter and it is the search text.
  • Lexical retrieval needing no vector database — portable across SQLite, MySQL, MariaDB and PostgreSQL with nothing installed. This is the shipped path, not a fallback.
  • EmbeddingProvider and VectorStore contracts, a portable brute-force store, and a pgvector adapter with its own mandatory CI leg. A store is an accelerator, never an authority: everything it proposes is re-filtered by scope before it is returned, and an unreachable store degrades to lexical and records the degradation.
  • Curation. Credentials are never stored in any status; every claim about a person waits for a human. Forgetting hard-deletes the vector and soft-deletes the row. Expiry is a retrieval predicate first and a sweep second.
  • remember and recall tools, and pandora:memory:sweep / :forget / :export.
  • Context pipeline: AttributeAllowlist (no toArray() path to a prompt), context files resolved against configured roots only, conversation summarisation as a stored artefact, and redaction inside ContextBuilder.
  • Workspaces with path containment checked after resolution on every operation, quotas claimed by conditional increment, and MIME matched on the detected type rather than the extension — built, and deferred to Phase 7 behind pandora.features.workspaces (off). The code and its tests stay in the tree; what is withheld is the way in, from everybody, including an operator holding every ability. Deferred because creating a workspace means choosing a root, and a UI field that accepts a root path accepts /.
  • Memory page, and the agent's Skills and Memory tabs. The Workspaces page and the agent's Workspace tab say the feature is coming.

Fixed

  • A global memory written inside a tenant became permanently invisible: BelongsToTenant stamps tenant_id on creating and the guard ran on saving, one hook too early.
  • PostgreSQL retrieval silently missed everything written with a capital letter — its LIKE is case-sensitive and the other three engines' are not.
  • A write through a symlink escaped the workspace root, because resolving a path for creation checked only the parent directory.

Changed

  • pandora:install runs the migrations when it is not interactive. --no-interaction means "take the default answers", and the default answer is yes; it used to print "not run (non-interactive)" and exit 0 with no schema, leaving a scripted install no error to detect. --no-migrate is still the way to opt out, and the command now verifies the schema exists afterwards rather than reporting success on the strength of having called migrate.
  • Migrations publish under a current timestamp via publishesMigrations(), following the application's database.migrations.update_date_on_publish setting. The packaged files are named 0001_01_01_* so they sort among themselves; a host that took those names verbatim could not order its own migrations relative to Pandora's.
  • pandora:agent:list reports a run count per agent, so the Agents page can be cross-checked against it.

Security

  • The Memory page disclosed every user's memories to every user. Reading it required only pandora.access — the ability an authenticated user holds by default — and the listing is filtered by memory scope and status, never by viewer. Reading now requires pandora.memory.manage, the same ability approving and forgetting already required, and the sidebar entry is filtered on it too. Found by the Phase 5 host walkthrough. Hosts that granted pandora.access broadly and pandora.memory.manage narrowly were affected; no configuration change is needed to take the fix.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator