Skip to content

API Capabilities

Dory Azar edited this page Nov 15, 2026 · 2 revisions

Services and capabilities

Caligrafy Quill 2.26.0. Generated from the source by php qa/build-api-reference.php; the guides explain how to use these classes, this reference says exactly what each public method takes and returns. Back to the API reference.

Build once: services, the capability registry, errors, the container, personal access tokens and the audit log.

Class What it is
Caligrafy\Service Extend it in application/services/, declare the inputs and implement handle()
Caligrafy\Capability #[Capability(name: 'create_book', description: 'Adds a book to the catalog', scope: 'books:write')] on a class that extends Caligrafy\Service.
Caligrafy\Capabilities A capability is a class of application/services/ that extends Service and carries #[Capability(...)].
Caligrafy\ServiceError A service throws it to refuse a call.
Caligrafy\Container Capabilities are made by the container, so a service asks for what it uses in its constructor
Caligrafy\Tokens A token is cq_<id>_<secret>: shown once when it is created, stored only as a SHA-256 hash in caligrafy_tokens with a name, an optional owner (users.id), its scopes, an optional expiry, the last use and a revocation ...
Caligrafy\Audit One row per capability call that changes data, and per refused call: when, who (a user, a token, the CLI), through which channel, which capability and the outcome.

abstract class Caligrafy\Service

namespace Caligrafy;

abstract class Service

Source: framework/core/Service.php:34

Class Service Extend it in application/services/, declare the inputs and implement handle():

#[Capability(name: 'create_book', description: 'Adds a book', scope: 'books:write')]
class CreateBook extends Service {
    public $rules = ['title' => 'required|max_len,255', 'author_id' => 'required|integer'];
    public $filters = ['title' => 'trim|sanitize_string'];
    public $descriptions = ['title' => 'The title of the book'];
    public function handle(array $input, Context $context) {
        return (new Book('books'))->create($input);
    }
}
  • $rules: GUMP validation rules per input. Only the inputs listed here reach handle(); use '' for an input without a rule. They also give the JSON Schema that agents and clients see (Capabilities::schema()). A file input takes the rule 'file' (2.19.0): 'cover' => 'required|file|extension,png;jpg|max_size,2mb'. Web pages and REST save the uploaded file and give handle() its public URL; MCP and the command line send a URL or a path (Caligrafy\Uploads).
  • $filters: GUMP filters applied before validation.
  • handle() returns data (an array, a model, a list of models, a scalar) and throws ServiceError to refuse. It never renders a view, redirects, prints or reads the request or the session: the context says who calls.

Public properties

  • public $rules = array(): array input => GUMP rules
  • public $filters = array(): array input => GUMP filters
  • public $descriptions = array(): array input => description, for the schema

Methods

->handle()

abstract public function handle(array $input, Context $context)

Does the work

Parameters

  • $input array: the validated and filtered inputs (only those of $rules)
  • $context Context: who calls, through which channel

Returns mixed: the result

Source: framework/core/Service.php:51

->validate()

public function validate(array $input)

Keeps the declared inputs, filters them and validates them

Parameters

  • $input array

Returns array: the clean inputs

  • @throws ServiceError invalid, with a message per field

Source: framework/core/Service.php:58

->capability()

protected function capability($name, array $input, Context $context)

Calls another capability as the same person (2.21.0)

Parameters

  • $name
  • $input array
  • $context Context

Source: framework/core/Service.php:99

->run()

public function run(array $input, ?Context $context = null)

No doc comment.

Parameters

  • $input array
  • $context ?Context, default null

Source: framework/core/Service.php:104

class Caligrafy\Capability

namespace Caligrafy;

class Capability

Source: framework/core/Capability.php:17

Attribute Capability #[Capability(name: 'create_book', description: 'Adds a book to the catalog', scope: 'books:write')] on a class that extends Caligrafy\Service. Its name is how REST, MCP, the JS client and the CLI call it.

Methods

->__construct()

public function __construct(
    public string $name,
    public string $description = '',
    public string $scope = '',
    public int $level = 0,
    public bool $readOnly = false,
    public bool $destructive = false,
    public bool $public = false,
    public bool $mcp = true,
    public bool $webOnly = false,
    public bool $async = false,
    public int $cache = 0
)

Parameters

  • $name string: snake_case, unique in the application (create_book)
  • $description string, default '': one or two sentences: what it does and returns (shown to agents as the tool description)
  • $scope string, default '': the token scope it needs (books:write); '' = none for a read-only one, its own name for a write one (2.26.0: a token needs the scope create_book, or *; TOKEN_DEFAULT_SCOPES=false, deprecated: none)
  • $level int, default 0: the minimum value of the users permission column (0 = any signed-in user)
  • $readOnly bool, default false: true when it changes nothing (not audited when it succeeds; agents may call it freely)
  • $destructive bool, default false: true when it deletes or overwrites data (agents ask before calling it)
  • $public bool, default false: true when anyone may call it without signing in or a token
  • $mcp bool, default true: false keeps it out of the MCP tools (one only a browser can use, such as a push subscription)
  • $webOnly bool, default false: true (2.21.0): only Route::action() reaches it: it may read the session and sign people in
  • $async bool, default false: true (2.23.0): Snap cards run it in the background as a task; REST, MCP, pages and call run it directly
  • $cache int, default 0: seconds (2.26.0): a GET of it over REST by a visitor with no session cookie and no token may be kept by shared caches (the production image's own cache, a CDN) for that long; needs readOnly and public, and an answer that is the same for every visitor

Source: framework/core/Capability.php:35

class Caligrafy\Capabilities

namespace Caligrafy;

class Capabilities

Source: framework/core/Capabilities.php:23

Class Capabilities A capability is a class of application/services/ that extends Service and carries #[Capability(...)]. Every channel calls it through call(), which always does the same, in order:

  1. finds it (not_found)
  2. checks the context: public, or signed in / a token (unauthenticated), with the scope and level (forbidden); a token needs the declared scope, or for a write capability that declares none its own name (scopeOf(), 2.26.0)
  3. keeps, filters and validates the declared inputs (invalid)
  4. runs handle()
  5. writes the audit log: every refusal, and every success of a capability that is not read-only and returns plain data (models become arrays of their columns).

Public properties

  • public static $defaultScopes = null: bool|null whether a write capability declared without a scope needs the scope named after it from a token (2.26.0): null until the first use reads TOKEN_DEFAULT_SCOPES (false, deprecated: any token, as before)

Methods

Capabilities::scopeOf()

public static function scopeOf(Capability $capability)

The scope a token needs to call a capability (2.26.0): the one it declares; for a write capability (not readOnly, not public) declared without one, its own name (create_book), which a token grants by that name or by *. Read-only and public capabilities without a scope need none. Signed-in users and the CLI never need a scope.

Parameters

  • $capability Capability

Source: framework/core/Capabilities.php:42

Capabilities::unscopedWrites()

public static function unscopedWrites()

The write capabilities declared without a scope, each with the scope a token now needs to call them (doctor)

Returns array: name => scope

Source: framework/core/Capabilities.php:60

Capabilities::register()

public static function register($class)

Registers a service class that is not in application/services/

Parameters

  • $class

Source: framework/core/Capabilities.php:75

Capabilities::all()

public static function all()

Every capability, by name

Returns array: name => ['class' => string, 'capability' => Capability]

Source: framework/core/Capabilities.php:85

Capabilities::get()

public static function get($name)

One capability (null when there is none of that name)

Parameters

  • $name

Source: framework/core/Capabilities.php:119

Capabilities::available()

public static function available(Context $context)

The capabilities a context may call (MCP lists only these)

Parameters

  • $context Context

Source: framework/core/Capabilities.php:127

Capabilities::call()

public static function call($name, array $input, Context $context, ?Uploads $uploads = null)

Calls a capability

Parameters

  • $name string: the capability name
  • $input array: the inputs (extra keys are ignored)
  • $context Context: who calls, through which channel
  • $uploads ?Uploads, default null: the files of the request (web and REST): saved once the caller is allowed, given to the capability as their URLs, deleted again when the call fails

Returns mixed: plain data

  • @throws ServiceError

Source: framework/core/Capabilities.php:145

Capabilities::schema()

public static function schema($name)

The JSON Schema of a capability's inputs, from its GUMP rules

Parameters

  • $name

Source: framework/core/Capabilities.php:195

Capabilities::schemaFromRules()

public static function schemaFromRules(array $rules, array $descriptions = array())

JSON Schema (draft 2020-12 subset) of GUMP rules

Parameters

  • $rules array: input => 'required|max_len,100|integer'
  • $descriptions array, default array(): input => description

Source: framework/core/Capabilities.php:210

Capabilities::describe()

public static function describe(?Context $context = null)

The description of every capability, as a list (php caligrafer.php capabilities, the REST listing)

Parameters

  • $context ?Context, default null: only the capabilities this context may call

Source: framework/core/Capabilities.php:273

Capabilities::plain()

public static function plain($value)

Plain data: models become arrays of their public columns, recursively

Parameters

  • $value

Source: framework/core/Capabilities.php:291

Capabilities::reset()

public static function reset()

Forgets the registry (tests)

Source: framework/core/Capabilities.php:350

class Caligrafy\ServiceError

namespace Caligrafy;

class ServiceError extends \Exception

Source: framework/core/ServiceError.php:16

Class ServiceError A service throws it to refuse a call. Each channel turns it into its own form: an HTTP status and a JSON error for REST, a tool error for MCP, the errors of a form for web pages.

Constants

  • STATUSES = array('invalid' => 422, 'unauthenticated' => 401, 'forbidden' => 403, 'not_found' => 404, 'conflict' => 409)

Public properties

  • public $kind: string invalid, unauthenticated, forbidden, not_found or conflict
  • public $fields: array field => message, for invalid input

Methods

->__construct()

public function __construct($kind, $message = '', array $fields = array())

No doc comment.

Parameters

  • $kind
  • $message, default ''
  • $fields array, default array()

Source: framework/core/ServiceError.php:26

ServiceError::invalid()

public static function invalid(array $fields, $message = 'Some of the inputs are invalid')

No doc comment.

Parameters

  • $fields array
  • $message, default 'Some of the inputs are invalid'

Source: framework/core/ServiceError.php:36

ServiceError::unauthenticated()

public static function unauthenticated($message = 'Sign in or send a personal access token')

No doc comment.

Parameters

  • $message, default 'Sign in or send a personal access token'

Source: framework/core/ServiceError.php:41

ServiceError::forbidden()

public static function forbidden($message = 'You are not allowed to do this')

No doc comment.

Parameters

  • $message, default 'You are not allowed to do this'

Source: framework/core/ServiceError.php:46

ServiceError::notFound()

public static function notFound($message = 'Not found')

No doc comment.

Parameters

  • $message, default 'Not found'

Source: framework/core/ServiceError.php:51

ServiceError::conflict()

public static function conflict($message)

No doc comment.

Parameters

  • $message

Source: framework/core/ServiceError.php:56

->status()

public function status()

The HTTP status of this error

Source: framework/core/ServiceError.php:64

->toArray()

public function toArray()

{"error": {"code": "invalid", "message": "...", "fields": {...}}}

Source: framework/core/ServiceError.php:72

class Caligrafy\Container

namespace Caligrafy;

class Container

Source: framework/core/Container.php:25

Class Container (2.18.0) Capabilities are made by the container, so a service asks for what it uses in its constructor:

public function __construct(private Mailer $mailer) {}

A class the container has no binding for is built with its constructor's parameters resolved the same way (a class or interface type -> Container::get, else the parameter's default). Bindings go in application/config/container.php:

Container::singleton(PaymentGateway::class, fn () => new StripeGateway(getenv('STRIPE_SECRET')));
Container::bind(Clock::class, SystemClock::class);

A test swaps one with Container::instance(Mailer::class, $fake), then Container::reset() forgets it.

Constants

  • FILE = 'config/container.php'

Methods

Container::bind()

public static function bind($id, $factory = null)

A new object each time: $factory is a callable returning it, or a class name (an interface bound to a class)

Parameters

  • $id
  • $factory, default null

Source: framework/core/Container.php:38

Container::singleton()

public static function singleton($id, $factory = null)

One object for the whole request, made the first time it is asked for

Parameters

  • $id
  • $factory, default null

Source: framework/core/Container.php:46

Container::instance()

public static function instance($id, $object)

This object for $id (a test's fake, for example)

Parameters

  • $id
  • $object

Source: framework/core/Container.php:54

Container::has()

public static function has($id)

Whether $id has a binding or an instance

Parameters

  • $id

Source: framework/core/Container.php:64

Container::get()

public static function get($id)

The object for $id: its instance, its binding, or the class built with its dependencies

Parameters

  • $id

Source: framework/core/Container.php:73

Container::make()

public static function make($class, array $parameters = array())

Builds $class (never a shared instance): the constructor parameters named in $parameters, then by type, then their defaults

Parameters

  • $class
  • $parameters array, default array()

Source: framework/core/Container.php:96

Container::boot()

public static function boot()

Loads application/config/container.php once (the web front controller does it after config.php; the CLI and tests the first time the container is used)

Source: framework/core/Container.php:132

Container::forgetInstances()

public static function forgetInstances()

Forgets the objects made so far, keeping the bindings (2.26.0: between two requests of the async server, so no shared instance carries one caller's state to the next)

Source: framework/core/Container.php:147

Container::keepInstances()

public static function keepInstances()

Keeps the instances made so far (the ones application/config/container.php registered at boot): forgetInstances() goes back to them

Source: framework/core/Container.php:157

Container::reset()

public static function reset()

Forgets every binding and instance (tests); the bindings file is read again on next use

Source: framework/core/Container.php:165

class Caligrafy\Tokens

namespace Caligrafy;

class Tokens

Source: framework/core/Tokens.php:20

Class Tokens A token is cq_<id>_<secret>: shown once when it is created, stored only as a SHA-256 hash in caligrafy_tokens with a name, an optional owner (users.id), its scopes, an optional expiry, the last use and a revocation date.

Scopes are <resource>:<action> strings (books:read, books:write). A token scope * grants everything and books:* grants every action on books. A capability's own name (create_book) is a scope too (2.26.0): the one a write capability declared without a scope needs (Capabilities::scopeOf()). Tables: framework/settings/platform.sql (php caligrafer.php platform install).

Constants

  • TABLE = 'caligrafy_tokens'
  • PREFIX = 'cq_'

Methods

Tokens::create()

public static function create($name, $scopes = array(), $userId = null, $expiresAt = null)

Creates a token

Parameters

  • $name string: what the token is for ("Claude Code on my laptop")
  • $scopes array, default array(): the scopes it grants
  • $userId int|null, default null: the user it acts for (null: the application itself)
  • $expiresAt int|string|null, default null: a Unix time (time() + 86400 * 90), or a local 'Y-m-d H:i:s' (null: never expires)

Returns array: ['id' => ..., 'token' => 'cq_...']: the token is never shown again

Source: framework/core/Tokens.php:33

Tokens::verify()

public static function verify($token)

Checks a token and records its use

Parameters

  • $token string: the token as sent (Authorization: Bearer cq_...)

Returns array|null: the token row (id, name, scopes as an array, user_id...) or null when unknown, revoked or expired

Source: framework/core/Tokens.php:54

Tokens::expired()

public static function expired($expiresAt, $now = null)

Whether an expiry date (local 'Y-m-d H:i:s', SERVER_TIMEZONE) has passed

Parameters

  • $expiresAt string|null: the expires_at column (null: never expires)
  • $now int|null, default null: a Unix time (null: now)

Source: framework/core/Tokens.php:79

Tokens::expiresAt()

public static function expiresAt($expiresAt)

The expires_at value of an expiry given as a Unix time. Dates are stored in local time (SERVER_TIMEZONE), and when the clocks fall back the repeated hour names two instants: strtotime() reads its first one. A token created at 01:21 EDT to expire an hour later (01:21 EST) would be refused at once, so an instant of the second occurrence is stored as the first local time after the repeated hour (02:00): it never stops early, at worst an hour late.

Parameters

  • $expiresAt int|string|null: a Unix time, a local 'Y-m-d H:i:s' (kept as given) or null (never expires)

Returns string|null

Source: framework/core/Tokens.php:96

Tokens::revoke()

public static function revoke($id)

Revokes a token

Parameters

  • $id

Returns bool: true when a token was revoked

Source: framework/core/Tokens.php:112

Tokens::all()

public static function all()

Lists the tokens (never their secret)

Returns array: rows, newest first

Source: framework/core/Tokens.php:126

Tokens::installed()

public static function installed()

Whether the framework tables exist (php caligrafer.php platform install)

Source: framework/core/Tokens.php:139

Tokens::looksLikeToken()

public static function looksLikeToken($token)

Whether a request carries a token of this format (as opposed to a legacy API key)

Parameters

  • $token

Source: framework/core/Tokens.php:152

Tokens::parse()

public static function parse($token)

The id of a well-formed token, null otherwise

Parameters

  • $token

Source: framework/core/Tokens.php:160

Tokens::allows()

public static function allows($granted, $required)

Whether granted scopes cover a required scope: exact, <resource>:* or *. An empty requirement is always met.

Parameters

  • $granted
  • $required

Source: framework/core/Tokens.php:168

Tokens::normalizeScopes()

public static function normalizeScopes($scopes)

Scopes as a sorted list of unique, valid strings (from an array or a space/comma separated string)

Parameters

  • $scopes

Source: framework/core/Tokens.php:188

class Caligrafy\Audit

namespace Caligrafy;

class Audit

Source: framework/core/Audit.php:17

Class Audit One row per capability call that changes data, and per refused call: when, who (a user, a token, the CLI), through which channel, which capability and the outcome. Inputs are never stored. Without the table (php caligrafer.php platform install) nothing is written and nothing fails.

Constants

  • TABLE = 'caligrafy_audit'

Methods

Audit::record()

public static function record(Context $context, $capability, $outcome, $detail = null)

Records one call

Parameters

  • $context Context: who called and through which channel
  • $capability string: the capability name
  • $outcome string: ok, invalid, unauthenticated, forbidden, not_found, conflict, error
  • $detail string|null, default null: a short note (an error message), at most 500 characters

Returns bool: whether the row was written

Source: framework/core/Audit.php:29

Audit::latest()

public static function latest($limit = 50, $capability = null)

The latest rows, newest first

Parameters

  • $limit int, default 50: how many
  • $capability string|null, default null: only this capability

Source: framework/core/Audit.php:46

Caligrafy: Describe it. Approve it. Ship it. Your idea turned into a complete app: every feature built once, for web pages, Vue apps, a REST API and AI agents, and online.

Start here

Why Caligrafy: the benefits, the measurements, and what it is not yet

  1. Quickstart with Claude: describe it, approve it, ship it

    or the Quickstart: the same three steps by hand, in 5 minutes

  2. How Caligrafy works: the ideas in 5 minutes
  3. Your first capability: build a feature, page included

Capabilities

Experiences

Data and people

  1. Models
  2. Relationships
  3. Queries: filters, paging, related rows
  4. Sign-in, users and tokens

Built-in features

Put it online

Build with AI

Reference

Clone this wiki locally