milpa / console
The projection layer of the Milpa PHP framework: surface projectors that turn one declared Operation into the shape each surface speaks — CLI flags, MCP tools — plus the signature gate that makes consent name the call.
Requires
- php: >=8.3
- milpa/command: >=0.12 <1.0
- milpa/core: >=0.12 <1.0
- milpa/http: >=0.1.5 <1.0
- milpa/live-tui: >=0.4 <1.0
- milpa/plugin: >=0.7 <1.0
- milpa/tool-runtime: >=0.17 <1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.65
- milpa/container: *
- nyholm/psr7: ^1.8
- phpstan/phpdoc-parser: ^2.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.20.0
- v0.18.4
- v0.18.3
- v0.18.2
- v0.18.1
- v0.18.0
- v0.17.0
- v0.16.1
- v0.16.0
- v0.15.0
- v0.14.1
- v0.14.0
- v0.13.0
- v0.12.0
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.2
- v0.10.1
- v0.10.0
- v0.9.3
- v0.9.2
- v0.9.1
- v0.9.0
- v0.8.1
- v0.8.0
- v0.7.10
- v0.7.9
- v0.7.8
- v0.7.7
- v0.7.6
- v0.7.5
- v0.7.4
- v0.7.3
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.0
- dev-rodrigoteamx/plugin-authority
- dev-fix/the-panel-speaks-english-by-default
- dev-fix/the-guard-was-case-sensitive-v2
- dev-fix/the-guard-was-case-sensitive
- dev-fix/the-cli-speaks-english-by-default
- dev-fix/console-does-not-use-milpa-live
- dev-fix/coercer-additional-properties
- dev-feat/operation-executed-carries-context
- dev-feat/persistent-confirm-token-store
- dev-fix/reach-command-0-7
- dev-fix/one-predicate-for-consent
This package is auto-updated.
Last update: 2026-09-12 00:37:46 UTC
README
Milpa Console
The projection layer of Milpa.
milpa/commanddeclares the atom — one surface-agnosticOperation; this package turns that atom into the shape each surface actually speaks. Today: CLI (flags, argument coercion, the signature gate) and MCP (tools an agent can call). One declaration, N surfaces.
Install
composer require milpa/console
Quick example
A plugin declares an operation once, through milpa/command's CommandProvider. It does not say
anything about flags, JSON-schema or terminals:
use Milpa\Command\Operation; new Operation( name: 'create_post', description: 'Create a draft post', handler: [PostService::class, 'create'], inputSchema: ['type' => 'object', 'properties' => ['title' => ['type' => 'string']]], mutating: true, requiresConfirmation: true, );
The projectors give it a surface:
use Milpa\Console\CliProjector; use Milpa\Console\McpProjector; // CLI: derives `--title=…` from the schema, coerces the string argv into typed input, and // enforces the signature gate before a mutating operation runs. (new CliProjector($authorizer))->run($operation, $argv, $container, $write); // MCP: the same operation becomes a tool an agent can list and call. (new McpProjector())->project($operations, $registry, $container);
Consent names the call
On a terminal, requiresConfirmation: true is not a --yes flag. A flag consents in the abstract —
the same yes covers removing any plugin on any host — so CliProjector asks for a signature that
names this call: the operation, its arguments, the host and a nonce. SchemaCoercer turns argv
strings into the types the schema declares before any of that, so what gets signed is what runs.
The pieces are seams, not concretions: OperationSigner is the port,
GnupgOperationSigner an adapter, and verification and nonce
spending live behind milpa/tool-runtime's OperationAuthorizer.
CliRunner accepts a signerAuthority resolver for the verified current signer. A recognized
signer's scopes pass through the shared PolicyGate before any handler receives a grant.
An explicit --sign also authenticates read operations: the handler receives attribution and
the caller's separate ToolContext, allowing a delegating driver to retain that authority.
A resolver failure refuses the call. Returning null retains the existing local signing behavior;
the host decides when that fallback applies. Stored session ownership supplies no caller authority.
CliRunner also accepts callerAuthority for an authenticated token. A host CallPolicy registered
in the container judges the concrete arguments before asking for a signature, and again with the
verified signer's authority. McpProjector installs that same policy in a concrete ToolRegistry.
Its OperationToolHandler forwards the registry's explicit context to OperationRunner; argument
payloads cannot supply that context.
A host may register OperationBoundary in the container. The runner passes the operation, input,
current authority and a closure for the declared handler to it on every surface. This is where the
host can require a confined executor; the ordinary handler runs only when the boundary calls its
closure. Delegating operations must carry the third handler argument into every child call.
Testing your own surfaces
Milpa\Console\Testing\SignsOperations ships in src/ on purpose: Composer does not autoload a
dependency's autoload-dev, so a test helper that lives in tests/ is unreachable for whoever
consumes the package. The trait hands you an always-signing signer and an accepting authorizer, so a
test that just needs to get past the gate can do so without a real key.
Where this is going
ADR-0035 — a projection is a value, not an effect —
governs this package. Today CliProjector::run() executes and McpProjector::project() registers;
neither returns a surface model, and that is the thing being retrofitted: a projector will produce a
model and a renderer will materialize it, so a surface can change its renderer without touching its
projector.
HTTP, without dragging identity in
Milpa\Console\Http\HttpProjector is the fourth surface: one route per operation, plus the generic
controller those routes point at. It arrived in 0.4.0 — until then it lived in milpa/skeleton,
because moving it as-is would have dragged milpa/auth into a floor meant to run without it.
What made the move possible is that admission sits behind an interface. The projector delegates
that decision to OperationHttpPolicy, and
milpa/admin publishes the implementation that uses
milpa/auth. Write your own and the projector will use it.
use Milpa\Console\Http\HttpProjector; // $psr17 is any PSR-17 factory pair you already have (Nyholm, Guzzle, Laminas…). $projector = new HttpProjector($operations, $container, $psr17, $psr17, policy: $yourPolicy); $routes = $projector->routes(); // hand these to your router $model = $projector->project($op); // or just ask what an operation would expose
Unlike the other optional collaborators in this family, not knowing is not permission here: an
operation that declares scopes with no policy wired throws UnguardedOperationException (a 500)
rather than running unguarded. It stays a 500 and never a 401/403 — the caller did nothing wrong; the
host declared something protected and left it without a guard. An operation that declares neither
scopes nor a permission never touches any of this.
When an HTTP operation delegates work to tools, its handler can accept a third optional argument,
?Milpa\ToolRuntime\Contracts\ToolContext $authority. The projector builds it from the authenticated request
and OperationRunner carries it alongside the second argument, InvocationContext. Attribution
and authority stay separate: the runner neither authorizes child calls nor stores a current user
in the container. The delegate must pass this authority to its tool gate. Missing authentication
or an empty scope list yields an empty list; it never inherits a local terminal's wildcard.
Requirements
- PHP >= 8.3
milpa/command,milpa/core,milpa/tool-runtime,milpa/httppsr/http-messageandpsr/http-factory— interfaces only. The HTTP projector asks for the PSR-17 factories instead of picking a PSR-7 implementation for you.
Contributing
See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
License
Apache-2.0 © Rodrigo Vicente - TeamX Agency. See LICENSE and NOTICE.
Milpa is designed, built, and maintained by Rodrigo Vicente - TeamX Agency.