Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,52 @@ This package is the wire contract between the Manager platform and the connector
A change here is a change to what a site may send, so every entry says what was added and - more
usefully - what was deliberately left out of it.

## 1.9.0 - 2026-08-17

A runtime report that can describe the Craft install, not just the disk it sits on.

`system.v1` and `system.v2` are untouched. A connector sending either keeps working against any
platform that accepted it before, and a platform that has not been upgraded keeps receiving exactly
what it received - the connector picks the newest version both sides know, so neither has to move
first.

### Added

- **`system.v3`.** Everything `system.v2` carries, plus four sections a site can answer about itself
and nothing outside it can observe:
- `craft` - the number of distinct deprecation warnings Craft has recorded, the row count of its
sessions table, whether a security key is configured, and whether the control panel is still at
the default trigger.
- `database.size_bytes` - the total including indexes. This is the number that decides whether a
backup finishes, and until now the report described every asset volume and the disk underneath
them while saying nothing about the thing being backed up.
- `paths` - whether Craft's `storage`, `cpresources` and project-config directories are writable.
- `php.missing_extensions` and `php.image_driver` - which of Craft's required extensions are
absent, and which imaging library is available.

### What was left out of it, and why

Every one of these has a more useful shape that this schema refuses.

- **A deprecation warning without its message is harder to act on.** The message names a template, a
file and a line, and those are the site's own code. The count says whether there is a problem;
finding it is done on the site, where the code already is.
- **A database size without a table breakdown is harder to act on.** A per-table row count describes
the shape of somebody's content, and a table *name* often describes their business. The total
answers the question a backup asks.
- **`cp_trigger_default` is a boolean, and there is no field for the trigger itself.** A site that
moved its control panel moved it somewhere it would rather not have written down; reporting the
new address in somebody else's dashboard would undo the change being reported.
- **`security_key_set` is presence, never the key.**
- **`missing_extensions` is a closed enum of Craft's published requirements**, so it cannot become
the inventory of loaded extensions that `php.extensions` deliberately reduces to a count. `paths`
is a fixed set of Craft's own directory names for the same reason a volume sends a handle rather
than a path: a free-form label is a filesystem path with extra steps.
- **`image_driver` is which library, never which version.** The version belongs to the host.
- **Response compression is not here at all.** It is decided at the edge, so a site behind a CDN
would report its origin's answer rather than the one a visitor receives. It is observed from
outside instead.

## 1.8.1 - 2026-08-09

No wire change. No schema, canonical string, signature or fixture moved, so this is interchangeable
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "coysh-digital/manager-protocol",
"description": "Wire contract shared by the Manager platform and the Manager Connector for Craft CMS.",
"version": "1.8.1",
"version": "1.9.0",
"type": "library",
"license": "MIT",
"keywords": [
Expand Down
36 changes: 36 additions & 0 deletions fixtures/system.v3/forbidden-content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"schema_version": "system.v3",
"collected_at": 1785400000,
"php": {
"version": "8.3.14",
"extensions": 47,
"loaded": ["core", "curl", "ioncube_loader", "newrelic"],
"missing_extensions": ["intl", "ioncube_loader"],
"image_driver": "imagick 3.7.0",
"ini_path": "/etc/php/8.3/fpm/php.ini"
},
"craft": {
"deprecation_count": 12,
"security_key_set": true,
"security_key": "vFq3mZ8pR2wK7nT4xL9dB6sG1hJ5cY0a",
"cp_trigger_default": false,
"cp_trigger": "acme-secret-panel",
"messages": [
"templates/_layouts/base.twig line 42: craft.request is deprecated",
"modules/acme/src/Payroll.php line 118: getUser() is deprecated"
]
},
"database": {
"size_bytes": 21390950,
"name": "acme_production",
"tables": [
{ "name": "acme_payroll", "rows": 4102 },
{ "name": "users", "rows": 918 }
]
},
"paths": {
"storage": true,
"path": "/var/www/vhosts/example.org/storage",
"web_root": "/var/www/vhosts/example.org/web"
}
}
53 changes: 53 additions & 0 deletions fixtures/system.v3/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"schema_version": "system.v3",
"collected_at": 1785400000,
"storage": {
"volumes": [
{ "handle": "images", "bytes": 4294967296, "files": 18402, "measured": true, "location": "local" },
{ "handle": "documents", "bytes": 214748364, "files": 903, "measured": true, "location": "local" },
{ "handle": "archive", "bytes": 0, "measured": false, "location": "remote", "unmeasured_reason": "remote" },
{ "handle": "video", "bytes": 8589934592, "files": 214, "measured": false, "location": "local", "unmeasured_reason": "timeout" },
{ "handle": "legacy", "bytes": 0, "measured": false, "location": "local", "unmeasured_reason": "unreadable" }
],
"storage_bytes": 1073741824,
"disk_free_bytes": 21474836480,
"disk_total_bytes": 107374182400
},
"php": {
"version": "8.3.14",
"sapi": "fpm-fcgi",
"memory_limit_bytes": 268435456,
"max_execution_time": 120,
"upload_max_filesize_bytes": 67108864,
"post_max_size_bytes": 67108864,
"max_input_vars": 1000,
"opcache_enabled": true,
"opcache_memory_used_bytes": 94371840,
"opcache_memory_free_bytes": 39845888,
"extensions": 47,
"missing_extensions": ["intl"],
"image_driver": "imagick"
},
"craft": {
"deprecation_count": 12,
"session_rows": 29,
"security_key_set": true,
"cp_trigger_default": false
},
"database": {
"size_bytes": 21390950
},
"paths": {
"storage": true,
"cpresources": true,
"config_project": false
},
"response": {
"samples": 200,
"window_hours": 6,
"mean_ms": 84.3,
"p50_ms": 71.0,
"p95_ms": 212.5,
"max_ms": 1840.0
}
}
188 changes: 188 additions & 0 deletions schemas/system.v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://protocol.coysh.digital/system.v3.json",
"title": "system.v3",
"description": "system.v2 plus what the site can say about its own Craft install that nothing outside it can observe: how much deprecated code is still running, how large the database is, whether the directories Craft writes to are writable, whether a security key is set, whether the control panel is still at the default address, and which of Craft's required PHP extensions are absent.\n\nEvery addition is a count, a boolean or a member of a closed set, and each one was chosen over a more useful shape for the same reason v2's `location` is two values rather than an adapter name.\n\n`security_key_set` is presence, never the key. `cp_trigger_default` says whether the trigger equals Craft's published default and never what it actually is - a site that moved its control panel moved it to somewhere it would rather not have written down in somebody else's dashboard, and reporting the new address would undo the thing it did. `paths` names Craft's own directories from a fixed list, so a site cannot report a filesystem path by choosing a label. `missing_extensions` is drawn from an enum of Craft's published requirements, so it can only ever say which of a public, fixed list is absent - the count in `extensions` stays exactly as it was, because the full list of what is loaded fingerprints the host and this does not.\n\nWhat is deliberately not here: response compression. It is a property of what the server sends a visitor, it is decided at the edge, and a site behind a CDN would report its origin's answer rather than the one anybody receives. It is observed from outside instead.",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "collected_at"],
"properties": {
"schema_version": {
"type": "string",
"enum": ["system.v3"]
},
"collected_at": {
"description": "Unix timestamp at which the connector gathered this data.",
"type": "integer"
},

"storage": {
"description": "Sizes of the volumes Craft manages. Labels are a closed set, so a site cannot name a directory here and thereby report a path.",
"type": "object",
"additionalProperties": false,
"properties": {
"volumes": {
"type": "array",
"maxItems": 50,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["handle", "bytes"],
"properties": {
"handle": {
"description": "The asset volume's handle. Not its path.",
"type": "string",
"maxLength": 128,
"pattern": "^[A-Za-z0-9._-]+$"
},
"bytes": { "type": "integer", "minimum": 0 },
"files": { "type": "integer", "minimum": 0 },
"measured": {
"description": "False when the volume could not be walked. A size of zero and a size nobody could measure are different facts, and `unmeasured_reason` says which of the several causes applied.",
"type": "boolean"
},
"location": {
"description": "Where this volume's files are. `local` means the server's own filesystem, so the bytes count towards its disk; `remote` means an object store or anything else reached over the network, so they do not and the disk figures elsewhere in this report say nothing about them. Two values on purpose - see system.v2's description for why it is not the adapter's name.",
"type": "string",
"enum": ["local", "remote"]
},
"unmeasured_reason": {
"description": "Why `measured` is false. `remote` - the files are not on this server and walking them would mean an API call per directory, billed to the site, to satisfy a dashboard. `timeout` - the walk started and ran out of the connector's storageWalkSeconds budget, so `bytes` and `files` are a floor rather than a total. `unreadable` - the path resolved to nothing this process could open, which is a configuration fault worth someone looking at. Absent when `measured` is true.",
"type": "string",
"enum": ["remote", "timeout", "unreadable"]
}
}
}
},

"storage_bytes": {
"description": "Craft's own storage directory: logs, caches, compiled templates, backups it has taken itself.",
"type": "integer",
"minimum": 0
},
"disk_free_bytes": { "type": "integer", "minimum": 0 },
"disk_total_bytes": { "type": "integer", "minimum": 0 }
}
},

"php": {
"description": "Runtime limits, as numbers. Never phpinfo(), never a path, never an ini file, and never the value of any setting that could name one.",
"type": "object",
"additionalProperties": false,
"properties": {
"version": { "type": "string", "maxLength": 32 },
"sapi": { "type": "string", "maxLength": 32 },
"memory_limit_bytes": {
"description": "-1 where the limit is unlimited, which is a real setting and not an error.",
"type": "integer",
"minimum": -1
},
"max_execution_time": { "type": "integer", "minimum": 0 },
"upload_max_filesize_bytes": { "type": "integer", "minimum": 0 },
"post_max_size_bytes": { "type": "integer", "minimum": 0 },
"max_input_vars": { "type": "integer", "minimum": 0 },
"opcache_enabled": { "type": "boolean" },
"opcache_memory_used_bytes": { "type": "integer", "minimum": 0 },
"opcache_memory_free_bytes": { "type": "integer", "minimum": 0 },
"extensions": {
"description": "Count only. The list itself would fingerprint the host in a way a byte count does not, and `missing_extensions` does not change that - it answers a closed question about a public list rather than describing what is installed.",
"type": "integer",
"minimum": 0
},
"missing_extensions": {
"description": "Which of Craft's required extensions are not loaded. A closed enum rather than free strings, so this cannot become the inventory of loaded extensions that `extensions` deliberately refuses to be. An empty array and an absent field mean different things: empty is a site that checked and found none missing, absent is a connector that did not look.\n\nThe imaging extensions are not in the list. Craft needs one of two rather than both, so \"missing\" is the wrong question about them and `image_driver` answers the right one.",
"type": "array",
"maxItems": 14,
"items": {
"type": "string",
"enum": [
"ctype",
"curl",
"dom",
"fileinfo",
"iconv",
"intl",
"json",
"mbstring",
"openssl",
"pcre",
"pdo",
"reflection",
"spl",
"zip"
]
}
},
"image_driver": {
"description": "Which imaging library Craft has available. `none` is a real answer and a serious one - a site in that state cannot generate a transform. Three values, and never a version string: the version belongs to the host, and what a reader can act on is which of the two libraries is in play.",
"type": "string",
"enum": ["gd", "imagick", "none"]
}
}
},

"craft": {
"description": "Facts about the Craft install that only the site can see. Counts and booleans; no setting's value appears here.",
"type": "object",
"additionalProperties": false,
"properties": {
"deprecation_count": {
"description": "How many distinct deprecation warnings Craft has recorded. The count, never the messages - a deprecation warning names a template, a file and a line, and those are the site's code.",
"type": "integer",
"minimum": 0
},
"session_rows": {
"description": "Rows in Craft's sessions table. A number nobody looks at until it is in the millions, which is what an install that has never run garbage collection looks like. No session, no token and no user id travels with it.",
"type": "integer",
"minimum": 0
},
"security_key_set": {
"description": "Whether a security key is configured. Presence, never the key. False means every token, session and encrypted value on the site is built on nothing, which is worth a platform saying out loud.",
"type": "boolean"
},
"cp_trigger_default": {
"description": "Whether the control panel is still reachable at Craft's default trigger. True means it is. False means somebody moved it, and this schema does not say where - see this schema's description for why reporting the new address would undo the change it is reporting.",
"type": "boolean"
}
}
},

"database": {
"description": "How large the database is. A size, never a table listing and never a row count per table: the first is what decides whether a backup finishes, and the second starts describing the shape of somebody's content.",
"type": "object",
"additionalProperties": false,
"properties": {
"size_bytes": {
"description": "Total size including indexes, as the engine reports it. Approximate on MySQL and MariaDB, which is fine for the question it answers - a backup that has grown from 200 MB to 9 GB is a different backup, and nobody needs the exact byte to see that.",
"type": "integer",
"minimum": 0
}
}
},

"paths": {
"description": "Whether the directories Craft has to write to are writable. A fixed set of Craft's own directory names, so a site cannot report a filesystem path by choosing a label - which is the same reason a volume sends a handle rather than a path. Absent for a connector that did not look; a directory absent from a present object is one it could not determine.",
"type": "object",
"additionalProperties": false,
"properties": {
"storage": { "type": "boolean" },
"cpresources": { "type": "boolean" },
"config_project": { "type": "boolean" }
}
},

"response": {
"description": "How long the site took to build its own responses, sampled on the site from traffic that was happening anyway. This is server processing time - the connector cannot see network, TLS or queueing - so it is not time to first byte and the interface does not call it that.",
"type": "object",
"additionalProperties": false,
"properties": {
"samples": { "type": "integer", "minimum": 0 },
"window_hours": { "type": "integer", "minimum": 1, "maximum": 168 },
"mean_ms": { "type": "number", "minimum": 0 },
"p50_ms": { "type": "number", "minimum": 0 },
"p95_ms": { "type": "number", "minimum": 0 },
"max_ms": { "type": "number", "minimum": 0 }
}
}
}
}
2 changes: 1 addition & 1 deletion src/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class Protocol
/**
* @var string Package version. Independent of the platform and connector versions.
*/
public const VERSION = '1.8.1';
public const VERSION = '1.9.0';

/**
* @var string Prefix on the canonical string a connector signs.
Expand Down
Loading