Download the PHP package milpa/agent-workspace without Composer
On this page you can find all versions of the php package milpa/agent-workspace. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Table of contents
Download milpa/agent-workspace
More information about milpa/agent-workspace
Files in milpa/agent-workspace
Download milpa/agent-workspace
More information about milpa/agent-workspace
Files in milpa/agent-workspace
Vendor milpa
Package agent-workspace
Short Description The agent's workspace for the Milpa PHP framework: the conversation, the composer, the gate, the work board and the decisions inbox — the room where a human meets the agent, declared as a section of the Milpa panel. Opt-in: an app has a panel without it.
License Apache-2.0
Package agent-workspace
Short Description The agent's workspace for the Milpa PHP framework: the conversation, the composer, the gate, the work board and the decisions inbox — the room where a human meets the agent, declared as a section of the Milpa panel. Opt-in: an app has a panel without it.
License Apache-2.0
Please rate this library. Is it a good library?
Informations about the package agent-workspace
# milpa/agent-workspace
**The room where a human meets the agent — a section of the Milpa panel, not the panel.**
The panel a human opens is the **Milpa Desktop**, and it comes from [`milpa/admin`](https://github.com/getmilpa/admin):
chrome, sections, the gate, settings, i18n. This package is a **tenant of it**. It brings the agent's workspace —
the conversation, the composer, the gate that parks a decision, the work board, the activity stream and the
decisions inbox — declared as one section named *Agent*.
**It is opt-in, and that is the point.** An app installs the panel and has **no agent** until it declares this.
The framework does not impose an agent on someone who only wants to administer their app.
> **Why the rename.** This package used to be called `milpa/agent-workspace`, and the name lied: it said *desktop*
> and delivered *agent*. Its own capability declaration claimed to provide `desktop-shell`, while the shell has
> always come from `milpa/admin` — which is six weeks older and holds the section contract, the gate and the
> i18n. See greenhouse `decisions/0220`.
## The routes are still `/desktop/*`, and that is not the old lie
`Desktop` is the **product**; `agent-workspace` is the **package**. The URLs name what a human opens, the package
names what it provides. Both are now true, which is the whole difference.
## Install
Then declare it in `config/plugins.php`. Installing the plugin *is* the activation; a Milpa without it
simply has no desktop shell.
## Run it end to end
From a fresh Milpa app to the shell in a browser — the whole path, proven on a fresh app
(greenhouse `evidence/0487`):
The origin is `localhost`, not `127.0.0.1`: WebAuthn accepts `localhost` as a relying-party id and refuses an
IP, so a passkey gate in front of the Desktop (below) only matches an origin spelled that way. The server is
still *bound* on the IP: PHP's built-in server listens on one address family, and `php -S localhost:8080` lands
on `[::1]` alone wherever the name resolves to IPv6 first, refusing every IPv4 client — a browser opening
`localhost` reaches a `127.0.0.1` bind with family fallback, and the passkey `rpId` matches the name either way.
Step 4 needs a `public/router.php` so the built-in server hands non-file requests (the shell, and the
`/desktop/assets/*.css` served by a route, not from disk) to the Kernel:
A real deployment (nginx/Caddy/Apache) needs no router — this is only the built-in server's convention.
## A native window (Electron)
`examples/electron/` is a minimal Electron host: it starts the app's `php -S` and loads `/desktop` in a
native window at a real origin — the Desktop is a Milpa serving itself, not an Electron app driving one.
## What it serves
- `GET /desktop` — the Milpa Desktop dashboard, served over HTTP. Point an Electron `loadURL` (or a
browser) at it. Built-in panels: the consent gate, the activity stream, and the passkey doors.
- `GET /desktop?session=` — the page bound to ONE agent session: the id the ledger's stream carries
(`desk-…`, `sequence:`). A reload keeps it, «New session» navigates to it, the inbox's «Open session»
reaches it, and the conversation is **replayed from the ledger** on every load — so a parked question is
answered in the thread it was raised in, and the turn continues after a reload. Without a name the
`milpa_agent_sid` cookie stands; without either, a fresh session is minted (greenhouse `evidence/0561`).
**Every surface reads that session from the ledger** — the header's goal and state, the counters (turns,
steps, tokens, tool calls, the context the last call held), the work board (the agent's todos, not dragged
by hand), the activity stream and the sidebar's list — folded by `LedgerSession` from
`var/agent-sessions.jsonl`. The Desktop's own record (`.milpa/sessions/.json`) is only the fallback
for a session the agent never ran, and an id nobody knows shows «No session open», never another
session's record.
- `GET /desktop?embed=1` — the same page in **embed mode**: the chrome folds and the shell fits one region of
a host page you own. Same route, same door. (The admin's Agent section does **not** use it any more — it
composes the Desktop's components inline; see below.)
- `GET /desktop/events` — the shell's live event feed (SSE), the transport when no hub is wired.
### Run a sequence, and answer its pause, from the inbox
A deployment is a list the app declares in `config/sequences.php` (greenhouse `decisions/0223`). The
**Decisions** screen lists those sequences and runs one from here: the card posts `sequence:run` through
the house's confirm gate (a 428 with a one-use token, then the same call with `Confirm-Token`), with the
passkey session as principal. When a step needs consent the run **pauses** — the card says at which step
and shows **Approve / Deny**, which post `agent:answer` to the session the run pauses in (`sequence:`);
an approval then resumes the run through the same gate. A session parked on a sequence from elsewhere (a
terminal) gets the same two buttons on its inbox card. The passkey must be enrolled with `agent:run` (to
start and resume) and `agent:answer` (to answer); nothing here bypasses a door a terminal would meet.
Every one of those routes — and the data, export, live and write endpoints — stands behind the door below.
Only the assets under `/desktop/assets/*` are public.
## Behind the door
The Desktop stands behind the same door as the admin (greenhouse `decisions/0209`). The plugin attaches the
PSR-15 middleware the app declares under **`desktop.middleware`** to every shell route; **since this version the
default answers only to loopback** (`Milpa\AgentWorkspace\Http\LoopbackOnlyMiddleware`): a request from the LAN gets
`403` — a small page for a browser, `{"ok":false,"error":"loopback_only"}` for the shell's own calls. Only a
literally empty list `[]` opens the Desktop. Anything misdeclared — a non-string entry, an associative map, a
value that is not a list, a class that does not exist or is not a PSR-15 middleware — makes the **whole** stack
fall to loopback-only, never open and never the half that loads; the topbar chip says `gate: fallback` in warning.
`desktop.locale` (`en` default, `es`) chooses the language of the chips and the notices.
To put it behind a passkey, name `milpa/app-runtime`'s gate — the Desktop does not import it; it names the class:
`PasskeyPlugin` must be declared in `config/plugins.php`, `'passkey' => ['rpId' => 'localhost']` set in
`config/app.php`, the credential declared in the out-of-band root (`config/identity.php` as
`['rooted' => ['']]` — enrollment *consumes* a root, it never mints one), and the key
enrolled with **every** scope the shell uses:
**All four, and `agent:answer` is the one that is easy to miss.** The gate checks `milpa.admin`, and turns
take `agent:run` — but answering a parked decision is a different authority and the runtime declares it
separately (`agent:answer`; the session reads take `agent:read`). Enrol with only the first two and the
Desktop signs you in perfectly and then refuses the **Approve** button with a `403` — authenticated,
unauthorized. Measured in a browser with a real key (greenhouse `decisions/0238`, `evidence/0578`), where
this README's earlier two-scope line was what produced the refusal.
A session bakes its scopes when it is minted, so **re-enrolling does not widen a session that is already
open**: sign out and in again after changing scopes, or the old ones keep deciding.
From then on identity replaces the address. A browser loading `/desktop` without a session is sent to
`/webauthn/signin?next=/desktop`; every `fetch()` the shell makes passes through one guard, so a gated call that
answers `401 {signin}` sends the browser to sign in and back (`next` carries the path and query), a `403` is told as
a system notice, and «Saved» is only ever said on a `2xx`. The topbar shows `signed in as ` — read from
the `milpa.auth` attribute the gate leaves on the request, never from a cookie — and `gate: passkey` when that class
is the whole stack (`loopback · custom · open · fallback` otherwise). The assets are exempt: a JSON `401` to a
`` or `