Document the ten new endpoint routes and undo the stray re-indent

Two of the three things holding the wilm import out of main.

The monitor CRUD, the two resolver reads and the three operator-report routes
had no entry in docs/api-inventory.json, so test_api_inventory_parity failed.
The gate cannot run on the published mirror - the test ships but its data file
is scrubbed, and it skips on the file's absence - which is why the branch that
added the routes never saw it. Cost lands here at promotion time instead.

/api/endpoints is registered both with and without a trailing slash and both
forms are served, so both are described rather than guessing which one counts.
Spec regenerated: 353 paths, 504 operations.

plugins/endpoints/plugin.py had a contiguous run of continuation lines shifted
by one space. Python accepted it and git diff -w showed nothing, but it put
deletions in a diff that is otherwise purely additive. Restored; the file now
differs from the published main by additions only.

Scope: product
This commit is contained in:
cproudlock
2026-09-02 13:49:54 -04:00
parent 27b2817fd7
commit 56ed89bb7b
3 changed files with 523 additions and 17 deletions

View File

@@ -3773,10 +3773,10 @@
"endpoints": [ "endpoints": [
{ {
"method": "GET", "method": "GET",
"path": "/api/endpoints", "path": "/api/endpoints/",
"auth": "endpoints.view", "auth": "endpoints.view",
"params": "query: search (hostname substring)", "params": "query: search (hostname substring)",
"purpose": "List endpoints reporting through the ShopDB agent, with last-seen and online state", "purpose": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here",
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints" "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints"
}, },
{ {
@@ -3922,6 +3922,86 @@
"params": "path: command_id; body: the command outcome (status, output)", "params": "path: command_id; body: the command outcome (status, output)",
"purpose": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state, caches that on the EndpointState so the UI toggle reflects reality between polls - both in one commit. Collector-token auth, not JWT", "purpose": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state, caches that on the EndpointState so the UI toggle reflects reality between polls - both in one commit. Collector-token auth, not JWT",
"example": "curl -X POST -H \"X-Collector-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"status\":\"succeeded\",\"output\":\"ok\"}' http://localhost:5001/api/endpoints/commands/1/result" "example": "curl -X POST -H \"X-Collector-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"status\":\"succeeded\",\"output\":\"ok\"}' http://localhost:5001/api/endpoints/commands/1/result"
},
{
"path": "/api/endpoints/monitors",
"method": "GET",
"auth": "jwt + require_permission endpoints.view",
"params": "none",
"purpose": "List every monitor definition, shipped and local, ordered shipped-first then by display name",
"example": "curl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\""
},
{
"path": "/api/endpoints/monitors",
"method": "POST",
"auth": "jwt + require_role admin",
"params": "body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)",
"purpose": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row",
"example": "curl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"kind\":\"service\",\"displayname\":\"Spooler\",\"servicename\":\"Spooler\"}'"
},
{
"path": "/api/endpoints/monitors/<monitor_id>",
"method": "PUT",
"auth": "jwt + require_role admin",
"params": "path: monitor_id; body: same fields as create, all optional",
"purpose": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only",
"example": "curl -X PUT http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'"
},
{
"path": "/api/endpoints/monitors/<monitor_id>",
"method": "DELETE",
"auth": "jwt + require_role admin",
"params": "path: monitor_id",
"purpose": "Delete a local monitor definition; 403 on a shipped row",
"example": "curl -X DELETE http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\""
},
{
"path": "/api/endpoints/monitors/resolve",
"method": "GET",
"auth": "jwt + require_permission endpoints.view",
"params": "query: pctype (required)",
"purpose": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy attaches to that type. The human-facing view of what the agent will be told",
"example": "curl 'http://localhost:5001/api/endpoints/monitors/resolve?pctype=Shopfloor+PC' -H \"Authorization: Bearer $TOK\""
},
{
"path": "/api/endpoints/monitors/resolved",
"method": "GET",
"auth": "endpoints collector API key (X-API-Key/Bearer), not JWT",
"params": "query: hostname (required)",
"purpose": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like its own MonitorConfig, so a policy-attached monitor is watched without editing appsettings.json on the bay. Unknown hostname is a 404; a PC type with no monitors returns empty lists",
"example": "curl 'http://localhost:5001/api/endpoints/monitors/resolved?hostname=F7MHPF24' -H \"X-API-Key: $ENDPOINTS_KEY\""
},
{
"path": "/api/endpoints/operatorreports",
"method": "POST",
"auth": "endpoints collector API key (X-API-Key/Bearer), not JWT",
"params": "multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)",
"purpose": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The screenshot is stored under a server-generated uuid filename; a retention sweep runs opportunistically so the feed stays bounded without a scheduler",
"example": "curl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $ENDPOINTS_KEY\" -F hostname=F7MHPF24 -F 'details=Machine will not home' -F screenshot=@shot.png"
},
{
"path": "/api/endpoints/panel/<asset_id>/operatorreports",
"method": "GET",
"auth": "jwt + require_permission endpoints.view",
"params": "path: asset_id",
"purpose": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows whether to request the screenshot",
"example": "curl http://localhost:5001/api/endpoints/panel/46/operatorreports -H \"Authorization: Bearer $TOK\""
},
{
"path": "/api/endpoints/operatorreports/<operatorreportid>/image",
"method": "GET",
"auth": "jwt + require_permission endpoints.view",
"params": "path: operatorreportid",
"purpose": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the report carries no image",
"example": "curl http://localhost:5001/api/endpoints/operatorreports/12/image -H \"Authorization: Bearer $TOK\" -o shot.png"
},
{
"method": "GET",
"path": "/api/endpoints",
"auth": "endpoints.view",
"params": "query: search (hostname substring)",
"purpose": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here",
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints"
} }
] ]
}, },

View File

@@ -21826,8 +21826,8 @@
"tags": [ "tags": [
"plugin-endpoints" "plugin-endpoints"
], ],
"summary": "List endpoints reporting through the ShopDB agent, with last-seen and online state", "summary": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here",
"description": "List endpoints reporting through the ShopDB agent, with last-seen and online state\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```", "description": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```",
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": []
@@ -22718,6 +22718,432 @@
} }
} }
}, },
"/api/endpoints/monitors": {
"get": {
"tags": [
"plugin-endpoints"
],
"summary": "List every monitor definition, shipped and local, ordered shipped-first then by display name",
"description": "List every monitor definition, shipped and local, ordered shipped-first then by display name\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
}
}
},
"post": {
"tags": [
"plugin-endpoints"
],
"summary": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row",
"description": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"kind\":\"service\",\"displayname\":\"Spooler\",\"servicename\":\"Spooler\"}'\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"description": "body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)"
}
}
}
}
}
},
"/api/endpoints/monitors/{monitor_id}": {
"put": {
"tags": [
"plugin-endpoints"
],
"summary": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only",
"description": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id; body: same fields as create, all optional\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
},
"404": {
"description": "No such record."
}
},
"parameters": [
{
"name": "monitor_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"description": "path: monitor_id; body: same fields as create, all optional"
}
}
}
}
},
"delete": {
"tags": [
"plugin-endpoints"
],
"summary": "Delete a local monitor definition; 403 on a shipped row",
"description": "Delete a local monitor definition; 403 on a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\"\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
},
"404": {
"description": "No such record."
}
},
"parameters": [
{
"name": "monitor_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/api/endpoints/monitors/resolve": {
"get": {
"tags": [
"plugin-endpoints"
],
"summary": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy...",
"description": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy attaches to that type. The human-facing view of what the agent will be told\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** query: pctype (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolve?pctype=Shopfloor+PC' -H \"Authorization: Bearer $TOK\"\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
}
}
}
},
"/api/endpoints/monitors/resolved": {
"get": {
"tags": [
"plugin-endpoints"
],
"summary": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like...",
"description": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like its own MonitorConfig, so a policy-attached monitor is watched without editing appsettings.json on the bay. Unknown hostname is a 404; a PC type with no monitors returns empty lists\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolved?hostname=F7MHPF24' -H \"X-API-Key: $ENDPOINTS_KEY\"\n```",
"security": [
{
"apiKeyAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
}
}
}
},
"/api/endpoints/operatorreports": {
"post": {
"tags": [
"plugin-endpoints"
],
"summary": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The...",
"description": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The screenshot is stored under a server-generated uuid filename; a retention sweep runs opportunistically so the feed stays bounded without a scheduler\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $ENDPOINTS_KEY\" -F hostname=F7MHPF24 -F 'details=Machine will not home' -F screenshot=@shot.png\n```",
"security": [
{
"apiKeyAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"description": "multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)"
}
}
}
}
}
},
"/api/endpoints/panel/{asset_id}/operatorreports": {
"get": {
"tags": [
"plugin-endpoints"
],
"summary": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows...",
"description": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows whether to request the screenshot\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/panel/46/operatorreports -H \"Authorization: Bearer $TOK\"\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
},
"404": {
"description": "No such record."
}
},
"parameters": [
{
"name": "asset_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/api/endpoints/operatorreports/{operatorreportid}/image": {
"get": {
"tags": [
"plugin-endpoints"
],
"summary": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the...",
"description": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the report carries no image\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: operatorreportid\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/operatorreports/12/image -H \"Authorization: Bearer $TOK\" -o shot.png\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
"content": {
"application/json": {
"$ref": "#/components/schemas/SuccessEnvelope"
}
}
},
"default": {
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"401": {
"description": "Missing or invalid credentials."
},
"403": {
"description": "Authenticated, but not permitted."
},
"404": {
"description": "No such record."
}
},
"parameters": [
{
"name": "operatorreportid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
},
"/api/scripts": { "/api/scripts": {
"get": { "get": {
"tags": [ "tags": [

View File

@@ -167,19 +167,19 @@ class EndpointsPlugin(BasePlugin):
# running a staged script or pushing an installer is a strictly # running a staged script or pushing an installer is a strictly
# heavier grant than a routine service/process toggle. # heavier grant than a routine service/process toggle.
('endpoints.deploy', ('endpoints.deploy',
'Run staged scripts and push application installers to endpoints', 'endpoints'), 'Run staged scripts and push application installers to endpoints', 'endpoints'),
# Packaging the agent is a heavier grant than deploying to one, and # Packaging the agent is a heavier grant than deploying to one, and
# it is not the same job either: the generated ZIP carries a live # it is not the same job either: the generated ZIP carries a live
# collector key stamped into its install script, so whoever holds # collector key stamped into its install script, so whoever holds
# this can mint a distributable artefact containing the fleet-wide # this can mint a distributable artefact containing the fleet-wide
# command-channel credential. It was require_role('admin'), which # command-channel credential. It was require_role('admin'), which
# this codebase otherwise reserves for the few genuinely # this codebase otherwise reserves for the few genuinely
# role-shaped checks - and a role cannot be granted to one person # role-shaped checks - and a role cannot be granted to one person
# for one job the way a permission can. # for one job the way a permission can.
('endpoints.agentpackage', ('endpoints.agentpackage',
'Generate and download an agent or adoption-tool package ' 'Generate and download an agent or adoption-tool package '
'(contains a live collector key)', 'endpoints'), '(contains a live collector key)', 'endpoints'),
# Collector service scope for the endpoint agent. A managed token # Collector service scope for the endpoint agent. A managed token
# scoped to ONLY this authorizes the endpoints collector and nothing # scoped to ONLY this authorizes the endpoints collector and nothing
# else, so rotating/revoking it stops endpoint-agent traffic without # else, so rotating/revoking it stops endpoint-agent traffic without
# touching computers/geenforce collectors. Minted from the settings # touching computers/geenforce collectors. Minted from the settings