Download the PHP package milpa/framework without Composer
On this page you can find all versions of the php package milpa/framework. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Table of contents
Download milpa/framework
More information about milpa/framework
Files in milpa/framework
Download milpa/framework
More information about milpa/framework
Files in milpa/framework
Vendor milpa
Package framework
Short Description The composer create-project starting point for a Milpa app: a runtime whose every capability is a declared Operation, reachable from the terminal, from MCP and from a TUI at once. No database, no web framework — an agent-operable substrate you extend with plugins.
License Apache-2.0
Package framework
Short Description The composer create-project starting point for a Milpa app: a runtime whose every capability is a declared Operation, reachable from the terminal, from MCP and from a TUI at once. No database, no web framework — an agent-operable substrate you extend with plugins.
License Apache-2.0
Please rate this library. Is it a good library?
Informations about the package framework
# milpa/framework
The `composer create-project` starting point for a Milpa app: a runtime where **every capability is a
declared Operation**, reachable from the terminal, from MCP and from a TUI at once.
No database. No web framework. No command framework. What you get is a substrate an agent can
operate, and a plugin system to grow it.
Run `php bin/coa house:start` for the house's current state and next steps. Enabling a capability
requires `--sign`, which authorizes that exact call with your key. Preview it with `--dry-run`:
New to the words? **[GUIDE.md](GUIDE.md)** takes a new house to a steady one, one command per step, naming
operations, signing, the constitution, capabilities, trials, seats and grants in the order you meet them.
## What `coa` is
`coa` does not implement any command. It boots the kernel, collects the operations that packages and
plugins declared, projects them onto the terminal and runs one:
That listing is **derived**, not written. Install a plugin that declares operations and they appear —
in `coa`, in the MCP tool registry, and in a TUI — with no edit to any file of this app.
## The one idea
A capability is declared **once**, as an `Operation`: a name, a description, an input schema, a
handler, and whether it mutates.
Return it from a plugin's `operations()` and you are done. A **projector** turns that declaration
into each surface's own shape — a command, an MCP tool, a TUI form — and a **renderer** materialises
it. Neither of them is something you write.
The corollary is the point: you never write the same capability twice, and you cannot have a
capability that the terminal offers and an agent cannot reach.
## The same operation, on four surfaces
A claim like that is cheap, so here is the whole of it on one install. `plugins.list` is declared
once, by the plugin-management plugin a newborn app already boots. Nobody wrote a controller, a
route, an MCP tool or a form for it. Output is trimmed to four fields for width and otherwise
untouched.
**Terminal** — a bare `composer create-project`, nothing else installed:
**MCP** — after `composer require milpa/mcp-server`, speak JSON-RPC to `php bin/coa mcp` (`bin/mcp-server.php` hands
to it). It serves the kernel of NOW: install, promote or disable a plugin, or write config, and the next call sees it
— no restart, and a client that honors `notifications/tools/list_changed` is told when the tools moved:
**HTTP** — name it in `config/http.php`, which serves nothing until you do:
That 401 is the surface working rather than failing. To get past it you need an identity, which is
its own opt-in — `composer require milpa/auth milpa/data`, and `token:*` appears:
A token holding the wrong scope answers `403` naming the scope it wanted. Neither the operation nor
the plugin knows any of it happened.
**TUI** — `php bin/coa shell`, every operation on one screen, this one among them.
One `new Operation(...)`, four answers. The envelope differs because each surface has its own — a
CLI result, an MCP content block, a bare HTTP body — and the payload underneath is the same object.
What a surface *may* show stays a decision, and it is declared where the capability is:
`Operation::supportsSurface()` reads a `surfaces:` argument that every projector consults, and the
TUI's own screen consults it too, so an operation kept off a surface is absent from it rather than
merely hidden. `plugins:disable-unsafe` declares `['cli']` because recovering a host is work for
whoever holds the terminal; the `agent` and `token:*` operations declare the same, and neither
exists at all until its capability is installed.
`config/http.php` starts empty for a different reason and it applies to every operation, not to
three: `coa` and MCP run on the machine of whoever invokes them, while a route can be called by
anyone who reaches the server. Exposing an operation to the network is a decision someone should be
able to read in a diff.
## Consent is part of the declaration
`mutating` says the operation changes something. `requiresConfirmation` says the change cannot be
undone, and on the terminal that means a **signature over this exact call** — the operation, these
arguments, this host:
The declaration travels with the operation, so every surface applies the same rule. It is not a flag
the terminal invented.
## Your key
`--sign` asks the `gpg` on your machine to sign the call. A new house asks for it early — founding the
house and enabling any capability both sign — so the first thing a new machine needs is a key that can
sign. If `gpg --list-secret-keys` prints nothing, create one once:
gpg asks for a passphrase; a hardware key (a YubiKey) works the same way. Without a usable key, `--sign`
refuses and says which case it met — no key in the keyring it read (and which keyring that was), a key
that could sign but whose prompt was declined, or no `gpg` at all — and what to do about it:
**A resident agent signs with a key of its own, never with yours.** Its signature is what the house
holds it to, so it lives in a separate keyring. Create it once per machine, without a passphrase (the
resident does not type one), and put `GNUPGHOME` in front of every command the resident runs:
## Two screens
`shell` lists the operations grouped by whether they read or change something, opens one with Enter,
lets you fill its fields and runs it — the same coercion and the same verdict the CLI uses. An
operation that demands a **signature** is not run from there: a signature names *this* call and is
produced with a key that lives outside the screen, so the form refuses and prints the exact
`coa … --sign` line instead. `Esc` goes back.
Neither is an operation, and neither pretends to be: an operation runs with what it was given and
answers. These converse. Piped or in CI they print one frame and exit — whether there is a terminal
is a fact of the destination, not of the screen.
## The agent
A new house has **no** `coa agent`: running it answers `no such command «agent»`. The agent is two
opt-ins — `milpa/ai-gateway` ships the loop that alternates model ↔ tools, `milpa/agent` the sessions it
runs in — and `coa agent` is the line that calls them. What it hands the model is not a separate
catalogue: it is **this app's operations**, the same ones an MCP client sees.
A run can change the house, so it is signed like any other lasting change; an unsigned `coa agent`
is refused before any model is asked. Without a key it says so and stops. There is no demo mode: an agent that answers something plausible
without having called anything teaches you to trust answers nobody produced. The answer comes back
with how many steps it took and how many tools it had, because "the agent replied" does not
distinguish using your app from replying from memory.
**Your own model works too.** Point it at anything OpenAI-compatible — an Ollama on your LAN, a
vLLM, a proxy — and no token leaves the building:
A declared endpoint wins over any provider key sitting in the environment: whoever pointed their
agent at a local model does not want a forgotten `ANTHROPIC_API_KEY` sending it elsewhere — and
billing them. For the same reason the endpoint is sent **`MILPA_AGENT_API_KEY` and nothing else**:
`OPENAI_API_KEY` is a secret for OpenAI, and the house never hands it to whatever host
`MILPA_AGENT_BASE_URL` names, even when that host speaks OpenAI's protocol. An endpoint that wants a key
and gets none answers 401, and the run says which variable to set.
It is a **terminal** operation only. An agent running over HTTP with the server's credentials is a
different decision, and this template does not take it for you.
### The agent is governed, and 0.13 closed the loop that measurement demanded
Three authorities sit between the model and your app, each answering exactly one question:
- **The floor** (`SessionPolicy`): does this need permission or a signature? No autonomy mode
skips a signature.
- **The intent contract** (ADR-0044): does a concrete operation even exist yet? An operation that
declares `namedTarget` gets enforced **before** the permission policy — `auto` waives asking
permission, not understanding what was asked. An unnamed target pauses the session with a formal
question that carries the operation and its arguments; the human's *yes* names that target and
only that target.
- **The second opinion** (`agent.secondOpinion` in `config/app.php`): does this concrete call go
beyond what was asked? Its denial removes the option from the session's table and the loop
continues with a different world.
And one rule that keeps all of it honest: **operations that adjudicate sessions are not tools of
the adjudicated**. `agent:answer` and `agent:mode` exist so a human governs the agent — they are
filtered out of the agent's own catalogue, because an agent that can answer its own pause or raise
its own autonomy is not governed; it is narrating governance.
When `agent.conditionalCatalog` is on, the run's result also says what the classifier decided
(`classifier: reads|changes|no-verdict|unreachable`) — the same reason `compacted` is reported:
anything that silently changes what the model sees, gets said.
## Serving operations over HTTP
The fourth surface. `config/http.php` names which operations get a route — and it is **empty**:
`coa` and MCP run on the machine of whoever invokes them; an HTTP route can be called by anyone who
reaches the server. Exposing everything by default would turn installing a plugin into publishing an
API nobody decided on. Same doctrine as `config/plugins.php`: what runs is a versioned decision.
### Identity, so the protected ones can be served too
Most operations worth exposing declare `scopes` — all twelve `plugins.*` do. This app wires the three
pieces that make them servable, in `config/boot.php` through `App\Http\IdentityWiring`:
1. a **token store** (`milpa/data`, file-backed by default — change the driver in `config/app.php`),
2. a **verifier** that turns `Authorization: Bearer …` into an actor with scopes,
3. a **policy** (`milpa/auth`) that decides whether that actor may run *this* operation.
Each is gated by the package it is made of, and by nothing else: the policy comes with `milpa/auth`
alone; the store and the verifier with `milpa/data`. So a house with a passkey door and no token store
still has a policy — and the passkey session, registered as the container's `AuthContextFactory`, is
the auth chain that policy asks for (greenhouse decisions/0208). A house with `milpa/auth` and
neither boots too; a scoped operation over HTTP then answers a `500` that names what is missing.
Mint a token, expose the operation, call it:
The token is **opaque**: 64 hex characters of random bytes. It is not a JWT — there is nothing in it
to decode. Who it belongs to and what it may do live in this house, under the token's hash, and
`token:revoke` takes effect because of that.
A token with the wrong scopes gets a 403 that names what was missing. `token:list` never prints the
secret or its hash, `token:revoke` takes effect on the next request, and the three token operations
are **terminal-only**: whoever can mint a token can mint one with every scope.
Remove the two `IdentityWiring` lines from `config/boot.php` and the app still runs whole over the
terminal and MCP — it just cannot expose anything that declares scopes, and the boot says so.
### What an operation must declare to be served — and why a read got a 428
An operation reaches HTTP through one chain:
`AuthenticateMiddleware` (from `milpa/auth`) turns `Authorization: Bearer …` into a verified actor and
leaves it on the request; `PasskeySessionMiddleware` (from `milpa/app-runtime`, present once the
passkey door is wired) does the same for the passkey session cookie; `RequestHandler` resolves the
route and runs the operation; `HttpProjector` (from `milpa/console`) coerces the input against the
operation's schema and shapes the response. The two identity slots are composed by
`App\Http\IdentityChain` (`src/Http/IdentityChain.php`) from what the container holds — a fresh app
holds neither and runs the bare handler. A middleware of your own hooks **in `public/index.php`**, as
the handler the chain ends in — a PSR-15 middleware is not a handler, so wrap it in one:
**The passkey session is a principal of the operations surface** (greenhouse decisions/0208). A
browser signed in through `/webauthn/signin` calls the same operations a Bearer does — `POST /agent`
included — with no token to paste. Three rules keep that honest: the Bearer goes *first*, and the
session yields to whatever it decided (a rejected token is never laundered by a cookie); a session
whose enrollment was revoked is destroyed on the spot, exactly as the panel gate does; and on a
mutating request (`POST`, `PUT`, `PATCH`, `DELETE`) the cookie counts only when the body is
`application/json` and, if the browser sends `Sec-Fetch-Site`, it says `same-origin` or `none` — a
cross-site form post arrives anonymous and the operation's policy answers. Driving the agent needs
the `agent:run` scope on either principal: `token:new --scopes=agent:run` or a passkey enrolled with
`--scopes=agent:run`.
**The one thing that is not optional: an `EffectProfile`.** Consent is decided by
`Consent::demanded($operation, $arguments)`, and it reads the operation's effect *ceiling*. An
operation that declares **no** `EffectProfile` has an *unknown* ceiling — and unknown is the
**highest** weight, not the lowest (an effect nobody classified must be treated as the worst case).
So an unclassified operation demands consent on **every** call, and a plain read served over HTTP
answers **428 Precondition Required** where you expected a 200. This is the seam `app-docentes`
hit first: `sync_pull`, a read, returned 428 until it declared its effects.
Declare them. A read says so, and is served without ceremony:
A write declares what it actually does, and consent applies by policy, not by accident:
The rule to carry: **an operation with no `EffectProfile` is not "harmless by default" — it is
"worst-case by default."** Declaring `readOnly()` is how a read earns its 200.
### Plugins that open connections: `boot()` runs on the caller's machine
`coa` and MCP run in-process on the machine of whoever invokes them, with no server between. That
promise has a sharp edge: a plugin's `boot()` runs on **every** invocation, including `coa list` and
every MCP handshake. A `boot()` that opens a database connection (Doctrine, PDO, a socket) makes
`coa` itself fail to start when that database is unreachable — the app becomes unrunnable from the
terminal because of a dependency the terminal never needed.
Keep `boot()` about **wiring, not connecting**. `registerService($id, Class::class)` with a
**class-string** is already lazy — the container instantiates it on the first `get()`, not at boot —
so a command that never touches the database never opens it. The anti-pattern is opening the
connection inside `boot()` itself, or handing `registerService` an already-connected **object**:
Make the connection object itself connect lazily (open on the first query, not in its constructor).
A plugin that genuinely needs a live connection before serving should open it in the request path,
never in `boot()`. The test that guards it is the cheapest one in the house: `coa list` must succeed
with the database down.
## Hooks
Every operation — from the terminal, the TUI, HTTP or MCP — runs through one seam that announces it:
`operation.executed` fires on **every** outcome — answered, served from a listener, stopped, thrown.
An audit trail with holes is worse than none: it teaches you to trust an incomplete list. And the
event carries the surface it came in through, because the same operation arriving over HTTP and on
the machine's own terminal does not always deserve the same answer.
## Running the suite
It ends with «OK, but some tests were skipped!» on a fresh app, and that is the expected reading: the
skipped tests measure opt-in capabilities — the agent, the model gateway, the generators, identity — and
each skip names the package it waits for. Switch a capability on (`php bin/coa capabilities:enable … --sign`)
and its tests stop skipping. A failure is a different word, and the suite says it.
## What is opt-in, and why
The box is deliberately small. Two examples of what it does **not** include:
- **Filesystem and shell primitives.** A framework that installs an agent with a shell by default is
a security decision, not a packaging one. `read`/`write`/`edit`/`grep`/`shell` live in a separate
plugin you install on purpose.
- **The remote plugin operations.** `plugins:list`, `:show`, `:enable` and `:disable` are here.
`:install`, `:update` and `:remove` appear only once you wire a `PluginInstallerInterface` — an app
that never reaches the network does not grow the operations that would.
## Layout
That is the whole of `src/`. The `agent` and `token:*` operations, the API-token store and its verifier,
and the conversation screen are not copied into your app: they ship in `milpa/app-runtime` and appear
when the capability they need is enabled (`coa capabilities` lists which).
`config/plugins.php` is a list, not a scan. What runs in this app is a versioned decision — a plugin
that installs itself from the network is an attack surface, not a convenience.
**Every Milpa is born with a constitution; not every Milpa needs a government yet.** The newborn's
only domain is *founding itself*: `.milpa/foundation.json` says what this app is — domain,
objective, boundaries, authorities — and it ships saying *not yet* (`domain: null`), with the
authorities already defaulting to the human: no app is born lawless, not even an unfounded one.
Declare the domain before scaffolding it; record the choices in `decisions/`; keep slice evidence
in `evidence/`. When the constitution needs to become verifiable, compiled law — ADRs with
supersession, enforced gates, corpus integrity — `milpa/governance` is one
`coa capabilities:enable governance` away, and it *adopts* the foundation as its antecedent, never
replaces it.
## Coming from an app you created before 0.21
Your app is not broken — it boots exactly as it did and every command still works. It is *frozen*:
26 of the 29 files in its `src/` were copied in the day you created it, and copied code never
receives an improvement again. From 0.21 those files ship as `milpa/app-runtime` instead.
**[UPGRADING.md](UPGRADING.md)** walks the migration: how to find out whether you edited any of them
first, then four steps and one check. Verified on a real 0.20.0 app — 29 operations before, the same
29 after.
## Coming from `milpa/skeleton`
`milpa/skeleton` was the previous `composer create-project` target and is now **abandoned** — this
package replaces it. Two entry points both saying *start here* is a second source of truth, and the
one a newcomer lands on decides what they believe Milpa is.
An app you already created from the skeleton keeps working: what it runs is your code, not that
package. If you are starting a new one, start here. What the skeleton brought and this does not are
`milpa/auth` and `milpa/data` — add them when your app needs them, which is the point of a floor you
build up from. Its one unique capability, the HTTP projection of operations, now lives in
`milpa/console` (with the auth-backed policy in `milpa/admin`).
## What this is NOT
It is not a web framework and it will not become one. There is no ORM, no template engine, no
router-with-batteries. `milpa/data` is here for one job — storing API tokens — and its four drivers
(file, sqlite, mysql, memory) are as much persistence as this floor takes a position on. `milpa/http` ships routing **contracts**; bring your own PSR-7 implementation
(this app ships `nyholm/psr7`) and your own persistence.
## Who this is not for
The list above says which features are missing. This one says which *architectures* are, because
choosing the operation as the atom is a choice about what the world is made of, and a reader
deserves to find that out here rather than three months in.
An `Operation` is a verb that answers once. That fits an app whose capabilities are things someone
— or something — decides to do, and it fits badly when the primitive is something else:
- **Event-sourced domains.** There is an event store in here, and the agent's own session state is
built on it, but it is machinery rather than an offered primitive. If `OrderPlaced` is the thing
your domain is made of, this floor gives you no natural home for it and you would be modelling
events as verbs in the past tense.
- **Long-running workflows.** An operation is a step, not a sequence. A three-stage payment that
survives restarts is not one operation and there is no orchestration primitive underneath it.
- **Streams and subscriptions.** An operation is called and returns. Continuous output — a feed, a
tail, a live subscription — is not a shape it has.
- **Rich stateful UI.** Operations are stateless by design and the TUI projects them one at a time.
An interface whose value is in its own accumulated state is not what a projector gives you.
And two limits that are current rather than architectural, stated so nobody discovers them by
surprise: a constitution is founded and amended by **one human authority** — the vocabulary is
`human`, there is no multi-signer deliberation — and there is **no demo mode**, so the agent will
tell you what it cannot reach instead of answering plausibly without having called anything. That
costs a first impression on purpose. An answer nobody produced teaches you to trust answers nobody
produced.
## The box is small; the runtime is federated
This package is deliberately small, and that is only half the sentence. What you end up **running**
is this floor plus whichever of the sibling packages your app turns out to need — identity,
persistence, agent sessions, the model gateway, the MCP server, the dev tools. The framework stays
a floor; the capability layer accumulates outside it, on purpose, so that installing a plugin is
never the same act as publishing an API.
The consequence is that nobody should have to discover that federation one refusal at a time. Ask
the app instead:
It lists what is installed, what is available, and the exact command that grows into each one.
## One atom is offered. The rest are substrate
«Every capability is a declared `Operation`» is true, and read as a description of the whole system
it is misleading. The Operation is the atom **you** declare. Underneath it the runtime stands on four
more primitives, and none of them is something you write — which is a choice, not an omission, and it
belongs here rather than in the surprise three months from now.
| primitive | what it is | can you declare one? |
|---|---|---|
| **Operation** | a capability: name, schema, handler, and what it may do at worst | **yes — this is the extension point** |
| **Effect ceiling** | `mutation × externality × reversibility × authority`, plus what escalates it and how it rolls back | yes, as part of an Operation |
| **Gate** | what withholds a tool until a condition holds: an unnamed target, an unmet prerequisite, an unfounded app | **no — there is no public contract** |
| **Event** | the append-only stream the agent's session is rebuilt from | you can implement the store; it is not an app-level primitive |
| **Foundation** | the constitution and the decisions that amend it — one per app, written by its rite | no, but you **run** it: `foundation`, `foundation:found` |
The line between the first two rows and the last three is *who acts*. An Operation is something a
human or an agent decides to do, so it is offered, projected and gated. A Gate is something the
**system** does, and offering it as a capability would mean shipping an app whose agent can widen the
rules it runs under. That is the same reason `agent:answer` and `agent:mode` are filtered out of the
catalogue the agent itself receives: an authority that can rewrite its own limit is not an authority.
The honest consequence: **if you are looking for the list of things this runtime is made of, it is
five rows and not one.** If you are looking for the list of things you write, it is the first row.
## License
Apache-2.0 © 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=framework)**.
All versions of framework with dependencies
PHP Build Version
Package Version
Requires
php Version
>=8.3
milpa/app-runtime Version >=0.198 <1.0
milpa/command Version >=0.5 <1.0
milpa/console Version >=0.13 <1.0
milpa/core Version >=0.6.2 <1.0
milpa/live-tui Version >=0.4 <1.0
milpa/plugin Version >=0.8 <1.0
milpa/runtime Version >=0.16 <1.0
milpa/tool-runtime Version >=0.13 <1.0
nyholm/psr7 Version ^1.8
nyholm/psr7-server Version ^1.1
milpa/live-web Version >=0.27 <1.0
milpa/app-runtime Version >=0.198 <1.0
milpa/command Version >=0.5 <1.0
milpa/console Version >=0.13 <1.0
milpa/core Version >=0.6.2 <1.0
milpa/live-tui Version >=0.4 <1.0
milpa/plugin Version >=0.8 <1.0
milpa/runtime Version >=0.16 <1.0
milpa/tool-runtime Version >=0.13 <1.0
nyholm/psr7 Version ^1.8
nyholm/psr7-server Version ^1.1
milpa/live-web Version >=0.27 <1.0
The package milpa/framework contains the following files
Loading the files please wait ...