agtp / agtp-drupal
Drupal module that wires AGTP handlers into the Drupal service container. Sites register tagged handler services; the module routes AGTP traffic through them via the gateway protocol.
Requires
- php: >=8.1
- agtp/agtp-php: ^0.1
- agtp/mod-php: ^0.1
- drupal/core: ^10.2 || ^11
Requires (Dev)
- drush/drush: ^12 || ^13
- phpunit/phpunit: ^10.0
This package is not auto-updated.
Last update: 2026-05-20 06:46:25 UTC
README
A Drupal module that exposes your site to the Agent Transfer Protocol (AGTP). Site builders write handler classes the same way they'd write Drupal services; AGTP traffic routes through them via the gateway protocol.
This module pairs with two other packages:
- agtp-php — the language
library that defines
EndpointContext,EndpointResponse,EndpointError, and the#[AgtpEndpoint]attribute. Handler classes use these directly. - mod_php — the runtime
that connects to
agtpdover a gateway socket. The drush command in this module wraps it. Lives in theagtp-phprepository alongside the SDK.
You do not run a separate agtpd daemon as part of Drupal — agtpd
is the AGTP server. You install it once on the host, and it listens on
TCP/4480 the same way Apache listens on 80. This module is the
Drupal-side worker that connects to it.
Why AGTP instead of JSON:API or REST?
You can already expose Drupal content over HTTP via JSON:API. So what does AGTP buy you?
A warm Drupal process per request. AGTP handlers run inside a
long-lived drush agtp:serve worker. Drupal's container is built
once at worker startup; every subsequent request reuses it. Cold
boot for a typical Drupal 10 site is 200-500 ms. Warm PHP-FPM behind
nginx is 50-150 ms per request. The AGTP worker is sub-millisecond per
request for the dispatch layer plus whatever the handler does — the
bootstrap tax is paid once at process start, not on every call. For
agent traffic — which is bursty and often hits the same endpoints
repeatedly — this is a measurable performance difference.
Identity and scope at the protocol level. $ctx->agentId is a
cryptographically verified agent identifier by the time it reaches your
handler. $ctx->authorityScope is the scope claim the daemon already
checked against the endpoint's declared requiredScopes. You don't
have to rebuild this with JWTs and middleware.
Attribution at the protocol level. Every method invocation produces a daemon-signed Attribution-Record. Audit logging is the transport's job, not yours.
HTTP keeps working. AGTP runs on its own port via agtpd. Drupal
answers HTTP on 80/443 as before. The two protocols coexist on the
same host without interfering.
Requirements
- Drupal 10.2+ or Drupal 11
- PHP 8.1+
agtpdrunning locally or on the same host- Drush 12+
Deployment compatibility
| Environment | Long-lived workers? | Status |
|---|---|---|
| Self-hosted (VPS, bare metal, Kubernetes, Docker Compose) | Yes — systemd, Supervisor, k8s Deployment |
Supported |
| Platform.sh | Yes — native worker containers | Recipe pending; should work |
| DDEV / Lando (local dev) | Yes — custom service overlays | Recipe pending; should work |
| Acquia Cloud | No native long-running workers | Not supported. Run agtpd + worker on a sibling instance. |
| Pantheon | Quicksilver is event-triggered only | Not supported. Same answer as Acquia. |
AGTP for Drupal is self-hosted-first. Sites on PaaS platforms
without long-running worker support need to run agtpd and the
drush agtp:serve worker on a sibling host they control, then point
the gateway socket at it via TCP loopback (127.0.0.1:4481) or over
the network.
Install
composer require agtp/agtp-drupal drush en agtp_drupal
Writing a handler
Three files: a handler class, a service registration tagging it
agtp.endpoint, and (for a fresh module) an info file. Drupal's DI
container collects everything tagged agtp.endpoint and feeds it to
the worker at boot.
1. The handler class
// web/modules/custom/example_agtp/src/Agtp/RoomHandlers.php namespace Drupal\example_agtp\Agtp; use Agtp\AgtpEndpoint; use Agtp\EndpointContext; use Agtp\EndpointError; use Agtp\EndpointResponse; use Drupal\Core\Entity\EntityTypeManagerInterface; final class RoomHandlers { public function __construct( private readonly EntityTypeManagerInterface $entityTypeManager, ) {} #[AgtpEndpoint( method: 'BOOK', path: '/room', errors: ['room_unavailable'], requiredScopes: ['booking:write'], )] public function book(EndpointContext $ctx): EndpointResponse|EndpointError { $nodes = $this->entityTypeManager ->getStorage('node') ->loadByProperties([ 'type' => 'room', 'field_room_type' => $ctx->input['room_type'] ?? 'double', ]); if ($nodes === []) { return new EndpointError( code: 'room_unavailable', message: 'No rooms of that type are bookable.', details: ['room_type' => $ctx->input['room_type'] ?? null], ); } $node = reset($nodes); return new EndpointResponse(body: [ 'reservation_id' => 'res-' . $node->id() . '-' . $ctx->agentId, 'room_id' => $node->id(), ]); } }
2. The service registration
Tag the handler service with agtp.endpoint. The collector picks it
up at boot.
# web/modules/custom/example_agtp/example_agtp.services.yml services: example_agtp.room_handlers: class: Drupal\example_agtp\Agtp\RoomHandlers arguments: - '@entity_type.manager' tags: - { name: agtp.endpoint }
3. The module info file
# web/modules/custom/example_agtp/example_agtp.info.yml name: Example AGTP handlers type: module package: AGTP core_version_requirement: ^10.2 || ^11 dependencies: - agtp:agtp_drupal
Enable: drush en example_agtp.
Generate the daemon manifest
After authoring handlers, project the #[AgtpEndpoint] attributes
into daemon-side endpoint TOML files. This closes the silent-drift
gap between the handler attribute and what agtpd is configured to
serve.
# Write one TOML per handler into the agtpd endpoints directory drush agtp:export-manifest --output=/etc/agtpd/endpoints # Or preview to stdout drush agtp:export-manifest --dry-run
The attribute is the source of truth. Re-run the command after every
handler change. A typical deploy script runs drush agtp:export-manifest
right after drush updb and before systemctl reload agtp-drupal.
Running the worker
drush agtp:serve --gateway-socket=/var/run/agtpd/gateway.sock
What happens:
- Drush bootstraps Drupal so the service container is built and your handler service is available.
AgtpHandlerCollectorwalks every service taggedagtp.endpointand callsHandlerRegistry::registerInstance()on each, picking up every method decorated with#[AgtpEndpoint].- A
GatewayClientconnects to the daemon, performs the handshake, receives the daemon's endpoint registration, and dispatches requests by looking up the registered handler. - The process serves until the daemon sends
goodbyeor the socket closes.
Production deployment
Run the worker under a process supervisor:
# /etc/systemd/system/agtp-drupal.service [Unit] Description=AGTP for Drupal worker After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/example.com ExecStart=/usr/bin/drush --root=/var/www/example.com/web agtp:serve --gateway-socket=/var/run/agtpd/gateway.sock Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target
For higher request concurrency, run multiple worker units — agtpd
accepts multiple module connections and routes among them.
Admin settings
After enabling, the settings page lives at
/admin/config/services/agtp. It shows the configured gateway socket,
the module identifier reported to agtpd, and a read-only listing of
every endpoint the service container has collected — useful as a
sanity check after deploy.
The page does not author handlers; handlers are PHP code in your custom modules. The page reflects what's in code.
Testing handlers
Use Agtp\Testing
to exercise handler methods directly. Build a synthetic
EndpointContext, call the method, assert on the result. No daemon,
no gateway socket, no AGTP traffic.
public function testBookSuccess(): void { $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class); // ... stub entityTypeManager as needed ... $handler = new RoomHandlers($entityTypeManager); $ctx = Testing::makeContext(input: ['room_type' => 'double']); $response = Testing::assertOk($handler->book($ctx)); $this->assertArrayHasKey('reservation_id', $response->body); }
What this module does not do
- Does not serve AGTP traffic over Drupal's HTTP request pipeline.
AGTP runs on its own port (4480) via
agtpd. Drupal answers HTTP on its usual port. The two protocols coexist on the same host. - Does not expose handler endpoints to anonymous traffic.
Authentication happens at the
agtpdlayer (Agent-ID resolution and, when Agent-Cert lands, mTLS). Inside the handler,$ctx->agentIdis the verified agent identity; trust it. - Does not provide a UI to author handlers. Handlers are PHP code in your modules. The admin page surfaces what code declared.
Related
agtp-php— the SDK and themod_phpruntimeagtp-symfony— the Symfony equivalent of this module