Download the PHP package milpa/tool-runtime without Composer
On this page you can find all versions of the php package milpa/tool-runtime. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Table of contents
Download milpa/tool-runtime
More information about milpa/tool-runtime
Files in milpa/tool-runtime
Download milpa/tool-runtime
More information about milpa/tool-runtime
Files in milpa/tool-runtime
Vendor milpa
Package tool-runtime
Short Description AI tool-execution runtime for the Milpa PHP framework: registry pipeline (resolve, validate, authorize, execute, audit), #[Tool] attributes, schema validation, policy gate, rate limiting, channel rendering, and human verification.
License Apache-2.0
Package tool-runtime
Short Description AI tool-execution runtime for the Milpa PHP framework: registry pipeline (resolve, validate, authorize, execute, audit), #[Tool] attributes, schema validation, policy gate, rate limiting, channel rendering, and human verification.
License Apache-2.0
Please rate this library. Is it a good library?
Informations about the package tool-runtime
# Milpa ToolRuntime
> The **AI tool-execution runtime** for the Milpa PHP framework, built on **`milpa/core`**. It runs the loop every Milpa module declares: `plugin → capability → tool → verification → event → result`. `#[Tool]`-attributed methods become a registry pipeline — resolve, validate, authorize, execute, audit — with policy gates, rate limiting, channel-aware rendering, and human/agent verification as first-class seams.
[](https://github.com/getmilpa/tool-runtime/actions/workflows/ci.yml)
[](https://packagist.org/packages/milpa/tool-runtime)
[](https://www.php.net/)
[](LICENSE)
[](https://getmilpa.github.io/tool-runtime/)
`milpa/tool-runtime` is where `milpa/core`'s agent-tool-readiness seam becomes a working
engine. `Milpa\Interfaces\Tooling\ToolProviderInterface` and `ToolRegistryInterface` are
contracts defined in core; this package is the concrete `ToolRegistry` that resolves,
validates, authorizes, executes, and audits every call, plus the `#[Tool]` attribute that
lets a plain PHP method declare itself as agent-callable. **No Doctrine, no HTTP kernel, no
concrete policy storage** — those live in your host application.
## Install
## Quick example
Attribute a method with `#[Tool]`; parameters describe themselves with `#[Param]`:
`ToolScanner` reflects the class for `#[Tool]` methods and registers them; `ToolRegistry`
runs the full pipeline on every call:
No `ToolContext` is required — `call()` defaults to `ToolContext::cli()` (full-access,
for scripts and tests). Real hosts build one per channel: `ToolContext::mcp($requestId,
$principal, $scopes)` for an authenticated MCP caller, `ToolContext::stdio($requestId)` for a
trusted local stdio MCP server process (no per-caller auth — see
[Authorize](#the-pipeline) below), `ToolContext::telegram($chatId, $userId)`, or a custom
`new ToolContext(...)` for a web session.
### Object-shaped parameters
A PHP `array` has no native type that maps to JSON-Schema `type: object` on its own —
without an override, `ToolScanner` infers `array` -> schema `type: array`, and
`SchemaValidator` then requires that value to be a JSON *list*, rejecting an associative
payload like `{"post_id": 1}` outright. Pass `type: 'object'` on `#[Param]` to opt a PHP
`array $param` into `type: object` instead; add `properties`/`requiredProperties` to declare
its shape (both optional — omit them for an open object with no declared shape):
This is purely opt-in (tool-runtime 0.6): a bare `array $param` with no `#[Param(type: ...)]`
override keeps generating `type: array` and keeps requiring a list, exactly as before.
### Parameter names on the wire
`ToolScanner` takes each wire argument name straight from `ReflectionParameter::getName()` —
there is no snake_case conversion. A PHP parameter `string $instanceId` produces the schema
property `instanceId`, not `instance_id`; a caller sending `instance_id` gets a "missing
required field" error. Every single-word parameter is unaffected (there is nothing to
convert), but a multi-word wire name must be matched by naming the PHP parameter itself in
that exact case — e.g. `string $instance_id` for a `snake_case`-conventioned tool family, or
`string $instanceId` for a `camelCase` one. Pick the PHP parameter name to match your tool
family's own wire convention; `#[Param]`'s `description`/`type`/etc. do not rename the
property.
## The pipeline
Every `ToolRegistry::call()` runs the same six steps, in order, regardless of who is
calling — a human over `cli`, an LLM over `mcp`, or a bot over `telegram`:
1. **Resolve** — look up the tool by name; an unknown name is a typed `ToolResult::error()`
(`ToolResult::TOOL_NOT_FOUND`), never an exception.
2. **Validate** — `SchemaValidator` checks the arguments against the tool's JSON input
schema (required fields, types), then applies numeric `clamps` before execution.
3. **Authorize** — `PolicyGate` checks the caller's `ToolContext` scopes against the tool's
required scopes, then falls back to per-channel policy (`cli` allows all, `mcp` and `web`
require auth by default). A host can plug in `PolicyRuleProviderInterface` for
database-backed rules, and an optional `RateLimiterInterface` throttles by
`channel:principal:tool`.
4. **Confirm** *(mutating tools only)* — a tool declared `confirm: true` (or matching a
channel's `require_confirmation_for_mutating` policy) returns a `confirm_token` on the
first call instead of executing; the caller replays the same arguments plus that token to
proceed. `ConfirmationTokenStore` holds the pending action and its expiry.
**The redemption contract, precisely:** on the first call, `ConfirmationTokenStore::create()`
snapshots the *exact args of that call* (name + args + a 60s-default expiry) and hands back a
random token. The caller is expected to replay the same arguments plus `confirm_token` on the
second call — but the runtime does not diff or validate that replay: `ToolRegistry::call()`
strips `confirm_token` off the incoming args, calls `ConfirmationTokenStore::consume($token,
$name)`, and — if the token is valid, unexpired, and minted for this tool name — **discards
whatever args the second call actually sent** and executes with the args stored at `create()`
time instead. A token is one-time-use (deleted on consume) and matched only by `$name`, not by
argument identity. Practically: the second call's args (other than `confirm_token` itself) are
inert — the tool executes with the *first* call's arguments, not the second's.
5. **Execute** — the tool's callback runs with a soft timeout; a bare return value is
wrapped in `ToolResult::success()` automatically, and an uncaught `Throwable` becomes
`ToolResult::error()` (`ToolResult::INTERNAL_ERROR`) instead of propagating.
6. **Audit** — `ToolAuditLogger` records every call (success, failure, or rejection) via
PSR-3, redacting sensitive argument fields (`password`, `token`, `secret`, …) before they
ever reach a log line. Since 0.5, the success/failure legs of this are event-driven — see
[Events](#events-toolexecuting--toolexecuted--toolfailed) below.
Since 0.5, one more thing happens between step 4 and step 5: if a `MilpaEventDispatcherInterface`
is wired, `ToolRegistry` dispatches `tool.executing` — a listener may short-circuit the call
(answer on the tool's behalf, e.g. a cache) or veto it outright, both without step 5 ever running.
See [Events](#events-toolexecuting--toolexecuted--toolfailed).
A `ToolContext` built with `mode: 'plan'` (or `ToolContext::asPlan()`) short-circuits after
step 3: it validates and authorizes but never executes, returning the would-be plan instead
— a dry-run for any tool, for free.
**Denials say which check failed.** When step 3 denies a call, `ToolResult::error()`'s
message names the specific check and what was missing — not a bare "forbidden". A channel
that requires auth reports `"channel 'mcp' requires an authenticated principal (require_auth)
— none provided."`; a scope mismatch reports `"Missing required scope for tool
'resolve_verification'. Need one of: verification:resolve — context has: tasks:write."`; a
`block_mutating` channel policy names the tool and the channel; a `PolicyRuleProviderInterface`
denial names the rule id, the tool, and the channel; a rate-limit denial names the exact
`channel:principal:tool` key that hit its budget. The error **code** stays
`ToolResult::FORBIDDEN` (or `ToolResult::RATE_LIMITED`) either way — callers that match on the
code are unaffected; only the message got specific enough to debug from the error alone.
**Trusted local stdio MCP servers**: a no-auth `mcp` transport (an editor or agent runtime
spawning your server as a child process, with no separate per-caller identity to authenticate)
should build its `ToolContext` with `ToolContext::stdio($requestId)` — it hard-codes
`principal: 'stdio'` and the wildcard `['*']` scope, the same "process boundary IS the trust
boundary" shape `ToolContext::cli()` already uses for CLI scripts. This exists because the
`mcp` channel's built-in policy sets `require_auth: true`: a bare `new ToolContext(channel:
'mcp')` (no `principal`) hits exactly the denial described above, one call at a time, with no
documented way out before this factory existed.
### Inspecting the pipeline: `InvocationPlan` + `coa:tools inspect`
The six steps above are the law that already governs every call. `InvocationPlan` makes that law
**statically visible without executing anything** — the read-only counterpart to plan-mode. Plan-mode
dry-runs *one call* (real args, real authorize, stops before Execute); `InvocationPlan` x-rays the
*wiring itself* — for a given tool on a given channel, which steps would run, which are inert, and
why — without a call at all.
`InvocationPlanBuilder::build(ToolDefinition $tool, ToolContext $ctx, RegistryWiring $wiring)` reads
a live `ToolRegistry` and emits an `InvocationPlan`: an ordered `list`, one per
pipeline stage (`Inspection\InvocationStepKind` enumerates all eleven — the six narrative steps above,
un-collapsed: `Validate`/`Clamp` split, `PlanMode`/`Confirm` split, plus `EmitExecuting`,
`ContainException`, and `Audit` as distinct stages). Each `InvocationStep` carries its
`InvocationStepRole` (`Guard`, `Transform`, `Branch`, `Hook`, `Execution`, `Boundary`, `Outcome` — so
`ContainException` is a `Boundary` that `wraps` Execute, and `Audit` is an `Outcome` that spans several
terminal paths, not a linear last step) and its `StepPresence`:
- **`Active`** — runs for this tool+channel.
- **`Conditional`** — may run depending on data known only at call time.
- **`Dormant`** — the rule exists but current static facts make it *impossible* to fire. The
`#[Tool]` attribute has no `mutating` parameter, so every scanned tool is `mutating: false`, which
leaves `block_mutating`, `require_confirmation_for_mutating`, and the 5×-rate-cost permanently
Dormant. The plan says so instead of pretending they guard anything.
- **`Skipped`** — the subsystem is not wired (no rate limiter, no dispatcher, no rule provider on
this registry).
The plan never guesses — it reports what the registry admits it has, through the read-only accessors
the builder derives presence from: `ToolRegistry::hasRateLimiter()` / `hasDispatcher()` and
`PolicyGate::channelPolicy()` / `hasRuleProvider()`. And it never fabricates a caller: with no actor
supplied on an auth-requiring channel, the context is genuinely anonymous (`principal: null`) and the
plan honestly predicts the denial. Its guiding rule (ADR#13): *inspection must describe what the
runtime actually executes, not an aspirational pipeline* — so `Audit`'s source enumerates its real
coverage (validation-failure, authorization-failure, rate-limit, cache-hit, execute-success,
execute-failure) and, just as honestly, the four terminal paths it does **not** audit (resolve-miss,
plan-mode, confirmation-request, veto).
`coa:tools inspect ` is the CLI projection of the same `InvocationPlan` — a host command in the
`MilpaToolRuntimePlugin` adapter (like `coa:tools`), not the runtime itself. `--channel` picks the
channel, `--actor`/`--scope` supply a caller to inspect (never fabricated when omitted), and `--json`
emits the whole plan (`schemaVersion`, `context`, `assumptions`, `wiring`, `steps`) for agents.
## Events: `tool.executing` / `tool.executed` / `tool.failed`
Since 0.5 (the event-driven retrofit), `ToolRegistry` accepts an optional
`Milpa\Interfaces\Event\MilpaEventDispatcherInterface` as its second constructor argument — the
same nullable-dispatcher-in-the-ctor pattern `HumanVerifier` already uses below. Without one,
`call()` behaves exactly as it did before 0.5: there is no interception point, and
`ToolAuditLogger` still logs every call (see below) — "no dispatcher" never means "no audit
trail", it only means "nothing can intercept."
With a dispatcher wired, three events fire around every call:
| Event | When | Carries | Slot? |
|-------|------|---------|-------|
| `tool.executing` | **PRE** — after resolve, validate/clamp, `PolicyGate::authorize()`, rate-limiting, and the confirm-gate have **all already run and passed** | `Events\ToolExecutingEvent($name, $ctx, $args)` | **Yes** — a `Milpa\Events\InterceptionSlot` travels alongside it |
| `tool.executed` | **POST** — a call finished, live or via a cache short-circuit | `Events\ToolExecutedEvent($name, $ctx, $args, $result, $cacheServed)` | No — readonly notification |
| `tool.failed` | **POST** — the tool's own callback threw | `Events\ToolFailedEvent($name, $ctx, $args, $exception, $tookMs)` | No — readonly notification |
### The security anchor
`tool.executing` is dispatched from exactly one place inside `ToolRegistry::call()`: *after*
every gate — resolve → validate/clamp → authorize → rate-limit → confirm — has already run and
said yes, and *before* `call_user_func()` ever invokes the tool's callback. This ordering is
load-bearing, not incidental: a `tool.executing` listener (a cache plugin, say) only gets a turn
once authorization has already cleared the call. A denied or rate-limited call returns before
`tool.executing` is ever dispatched — the listener is never invoked for it, full stop. Moving this
dispatch any earlier (e.g. "before validate" or "before authorize") would let a cache hit stand in
for an authorization check that never ran — an auth bypass wearing a cache's clothes.
### The cache-plugin recipe
A listener on `tool.executing` can answer on the tool's behalf via the `InterceptionSlot`
dispatched alongside the event — the tool's real callback never runs:
Because the anchor sits strictly after `authorize()`, this cache plugin can **never** answer a
call `PolicyGate` already denied — the security property, verified end-to-end (including a real
`EventDispatcher`, a real denied principal, and an assertion that the cache listener's invocation
count stays at zero) in `tests/Events/CacheShortCircuitTest.php`.
A listener may instead call `$slot->stop()` (without `shortCircuit()`) for a pure veto — `call()`
then returns `ToolResult::blocked('Tool execution vetoed by an event listener')` without ever
invoking the callback, and without a replacement result.
### `ToolAuditLogger` is a listener, not an imperative call
`ToolRegistry`'s constructor subscribes its internal `ToolAuditLogger` to `tool.executed` /
`tool.failed` whenever a dispatcher is supplied — `ToolAuditLogger::onToolExecuted()` /
`onToolFailed()` reproduce the exact log lines the registry used to emit imperatively (including
the soft-timeout warning, now driven off `$result->meta['timeout_exceeded']` instead of a direct
call). When **no** dispatcher is wired, `call()` invokes those same listener methods directly
instead of going through `dispatch()` — so "no dispatcher" still means "full audit trail", never
"silently stopped logging."
Validation failures, authorization denials, and rate-limit rejections keep logging exactly as
they did before 0.5 (`logValidationFailure()` / `logAuthFailure()` / a direct `log()` call in the
rate-limit branch) — those all happen *before* the security anchor, so there is no `tool.*` event
yet dispatched for them to hang off.
## Verification: `request_verification` / `resolve_verification`
Some actions can't be authorized by scopes alone — they need a human or another agent to
say yes. `milpa/core` defines the seam: `Milpa\Interfaces\Verification\VerifierInterface`,
whose `verify()` returns a `VerificationResult` that may be `PENDING` and resolve later.
This package ships the reference implementation:
- **`HumanVerifier`** implements `VerifierInterface`. `verify()` cannot decide
synchronously, so it returns `VerificationResult::pending()` and dispatches
`verification.requested`; a later `grant()` / `reject()` call resolves it and dispatches
`verification.granted` / `verification.rejected`.
- **`VerificationTool`** exposes `HumanVerifier` as **two** tools — the *same* registry
pipeline every other tool runs through, no special-cased transport:
- `request_verification(subject, policy?, requested_by?, request_id?)` opens a
verification and returns its `request_id`. Its schema has **no** `decision` or
`principal` field at all — this tool can never grant or reject anything, only open a
request.
- `resolve_verification(request_id, decision, principal, subject?, reason?)` resolves a
pending one. `request_id`, `decision` (`grant`|`reject`), and `principal` are
**required**; `subject` and `reason` are optional. When `subject` is omitted, the
resolved `VerificationRequest` is built via `VerificationRequest::forResolution()`
(`subject` stays `null`, identified purely by `request_id`) — the earlier `subject =
$request_id` fallback (tool-runtime 0.3) is gone, so a `verification.granted` /
`verification.rejected` listener never sees a fabricated subject. The tool's own success
*message* still shows `request_id` in place of `subject` for readability; that is
display-only formatting, not the `VerificationRequest`'s `subject` field.
Tool-runtime 0.2 shipped this as a single combined tool that mixed both phases behind one
schema, distinguished only by whether `decision` was present — a shape whose name also
invited reading it as "the caller can verify itself". 0.3 splits it into the two tools
above; the old combined tool no longer exists.
Both tools register with `ToolOptions(mutating: true, requiresConfirmation: false)` — the
registry's generic step-4 confirmation gate (see [The pipeline](#the-pipeline)) is
deliberately **bypassed** for both, because `handleRequest()` / `handleResolve()` together
already *are* the two-phase confirmation protocol (open a request, resolve it later).
Stacking the registry's confirm-token gate on top of that would recreate the confusing
3-4 call choreography tool-runtime 0.2 already killed for the combined tool — see
[Changed in 0.2: the double-gate bypass](#changed-in-02-the-double-gate-bypass) for that
history.
⚠️ The bypass is not absolute: a channel whose policy sets
`require_confirmation_for_mutating` (the built-in `telegram` policy does) still gates
**any** `mutating: true` tool via `PolicyGate::requiresConfirmation()`, regardless of the
tool's own `requiresConfirmation` flag. On `cli`, `mcp`, and `web` (none of which set that
policy by default) the bypass is total.
### Through the registry: request → resolve in two calls
Calling either tool via `$registry->call()` runs its handler directly — no generic
confirm-token wrapper in between. A full request → resolve round trip is exactly two calls,
one per tool:
Echo the `request_id` from the first call's response back on `resolve_verification` — it is
`HumanVerifier`'s own correlation id (#7), not the registry's `confirm_token`; neither tool
mints or expects a `confirm_token`.
### The policy dividend: restrict `resolve_*` without touching `request_*`
Because the two phases are separate tools — not one tool with a conditional field — a host's
policy can allow `request_verification` to any principal that can reach the registry while
restricting `resolve_verification` to specific principals, using the same `scopes` mechanism
every other tool in this package uses. `VerificationTool`'s constructor takes an optional
`resolveScopes` list, applied only to `resolve_verification`'s `ToolOptions`:
`tests/Verification/VerificationToolPolicyDividendTest.php` pins exactly this scenario.
### Direct usage: calling the handlers without a registry
The same request → resolve round trip is also reachable by calling `handleRequest()` /
`handleResolve()` directly, independent of any `ToolRegistry` — useful when you don't have a
registry at hand (e.g. a unit test):
Any other `VerifierInterface` implementation — a deterministic rule, a quorum vote, an
external approval service — plugs into the same seam.
### Changed in 0.2: the double-gate bypass
Before 0.2, the combined verification tool used `requiresConfirmation: true`, so **any** call
through `ToolRegistry::call()` — request or resolve alike — hit the registry's own step-4
confirmation gate *before* the tool's own handler ever ran. Opening a request took **two**
registry calls just to reach the request phase (which itself returned a confirmation, this
one carrying `request_id`) — and resolving it needed a **third**, itself gated the same way.
The registry's generic wrapper carries no `request_id` (it knows nothing about
`HumanVerifier`), so a caller only ever saw the correlation id after redeeming a token they
didn't know they'd need. 0.2 set `requiresConfirmation: false` instead, since the handler's
own `request_id` round trip already *is* the confirmation protocol — the registry's generic
one was pure overhead for this tool specifically. 0.3's split preserves that same
`requiresConfirmation: false` decision on both `request_verification` and
`resolve_verification` — see [Verification](#verification-request_verification--resolve_verification)
above.
### Events: what the payload actually carries
`HumanVerifier` dispatches three events — `verification.requested` (from `verify()`),
`verification.granted` and `verification.rejected` (from `grant()` / `reject()`) — through
the optional `MilpaEventDispatcherInterface` passed to its constructor. Every dispatch uses
the **same payload shape**: a single key, `'event'`, holding the event **object**, not a
flattened array of the request's fields:
A listener reaches the data through the event object's accessors, **not** array keys —
`$payload['subject']` is always `null`/undefined; the subject lives at
`$payload['event']->getRequest()->subject`:
| Event | Accessors |
|-------|-----------|
| `VerificationRequestedEvent` | `getRequest(): VerificationRequest`, `getRequestId(): ?string` |
| `VerificationGrantedEvent` | `getRequest(): VerificationRequest`, `getResult(): VerificationResult`, `getRequestId(): ?string` |
| `VerificationRejectedEvent` | `getRequest(): VerificationRequest`, `getResult(): VerificationResult`, `getRequestId(): ?string` |
A listener that wants to work with any of the three (or with a future verifier's events)
should branch on the event's class, or narrow via `getRequest()`/`getResult()`, rather than
assume a flat array — this is defined and enforced by the event classes' own docblocks in
`milpa/core` (`Milpa\Events\Verification{Requested,Granted,Rejected}Event`).
## What lives where
| Layer | Package | Owns |
|-------|---------|------|
| Contracts | `milpa/core` | `ToolProviderInterface`, `ToolRegistryInterface`, `VerifierInterface`, capability/verification value objects and events — the seams, not the engine. |
| **Runtime** | **`milpa/tool-runtime`** (this package) | The concrete `ToolRegistry` pipeline, `#[Tool]`/`#[Param]` attributes + `ToolScanner`, `SchemaValidator`, `PolicyGate`, rate limiting, channel rendering, `ToolAuditLogger`, the `HumanVerifier` reference verifier (`request_verification` / `resolve_verification`), and the read-only `Inspection\` pipeline model (`InvocationPlanBuilder` → `InvocationPlan`) behind `coa:tools inspect`. |
| Your app | your host / plugins | Concrete `PolicyRuleProviderInterface` (e.g. Doctrine-backed rules), `LoggerInterface`, channel renderers, and where policy decisions and audit logs are actually persisted. |
## API de facto
The types you construct and pass around day to day:
| Type | What it is |
|------|------------|
| `Contracts\ToolContext` | Who/where/what-scopes for one call — `principal`, `channel`, `scopes`, `mode`. Named constructors per channel: `cli()`, `mcp()`, `stdio()` (trusted local stdio MCP server), `telegram()`. |
| `ToolResult` | The uniform return shape — `success`, `data`, `message`, `error`, `meta`. Factories for common shapes: `success()`, `error()`, `paginated()`, `detail()`, `confirmation()`, `blocked()`. |
| `ToolRegistry` | The pipeline: `register()` to add a tool by hand, `call()` to run resolve→validate→authorize→execute→audit, `getToolSummaries()` (plain-array LLM/MCP wire shape) / `getToolDefinitions()` (typed `list`) / `getToolsWithinBudget()` for LLM/MCP exposure; read-only introspection via `hasRateLimiter()` / `hasDispatcher()` (and `PolicyGate::channelPolicy()` / `hasRuleProvider()`) — what the `Inspection\` builder reads without ever calling. |
| `Rendering\RendererRegistry` | Picks a `ChannelRendererInterface` for a `ToolResult` based on `ToolContext::$channel`, falling back to a default renderer or raw JSON. |
| `Contracts\LlmServiceInterface` | The seam a plugin implements to provide LLM access (`generateResponse()`) and other plugins consume to get one, without depending on a specific provider. |
| `Events\ToolExecutingEvent` / `Events\ToolExecutedEvent` / `Events\ToolFailedEvent` | The three `tool.*` event VOs (0.5) dispatched around `ToolRegistry::call()` — see [Events](#events-toolexecuting--toolexecuted--toolfailed). |
| `Inspection\InvocationPlanBuilder` → `Inspection\InvocationPlan` | Builds a read-only, per-tool/channel x-ray of the pipeline from a live `ToolRegistry`, executing nothing — an ordered `list` (each tagged by `InvocationStepKind` / `InvocationStepRole` / `StepPresence`) plus `RegistryWiring` (which optional collaborators are actually plugged in). Projected to the CLI by `coa:tools inspect`. |
## Requirements
- PHP **≥ 8.3**
- [`milpa/core`](https://packagist.org/packages/milpa/core) **^0.6** (the `InterceptionSlot` / `MilpaEventDispatcherInterface` keystone the [Events](#events-toolexecuting--toolexecuted--toolfailed) seam is built on)
- [`psr/log`](https://packagist.org/packages/psr/log) **^3**
- [`milpa/events`](https://packagist.org/packages/milpa/events) *(optional, dev-only)* — the reference `MilpaEventDispatcherInterface` implementation; any conformant implementation works, this package has no hard dependency on it
## Documentation
**Full API reference: [getmilpa.github.io/tool-runtime](https://getmilpa.github.io/tool-runtime/)** — generated
straight from the source DocBlocks and dressed with the Milpa design system.
## Contributing
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security
issues via [SECURITY.md](SECURITY.md), and note that this project follows a
[Code of Conduct](CODE_OF_CONDUCT.md).
## License
[Apache-2.0](LICENSE) © Rodrigo Vicente - TeamX Agency.
---
Milpa is designed, built, and maintained by **[Rodrigo Vicente - TeamX Agency](https://teamx.agency/?utm_source=github&utm_medium=readme&utm_campaign=milpa&utm_content=tool-runtime)**.
All versions of tool-runtime with dependencies
PHP Build Version
Package Version
Requires
php Version
>=8.3
milpa/core Version >=0.6.2 <1.0
psr/log Version ^3
milpa/command Version >=0.9.1 <1.0
milpa/core Version >=0.6.2 <1.0
psr/log Version ^3
milpa/command Version >=0.9.1 <1.0
The package milpa/tool-runtime contains the following files
Loading the files please wait ...