Bring the wilm branch into main: endpoints plugin, agent, policies, CSV import
Ports the corporate-GitHub wilm branch onto main, keeping the two branches
separate. Thirteen upstream commits arrive with their authorship intact; the
review fixes and the portability work below are on top.
What arrives:
endpoints plugin - a ShopDB agent reporting endpoint state, telemetry,
services and interfaces, with a command channel the agent
long-polls. Ingest stays on the generic collector route
(ADR-006); the agent key is a UI-managed single-scope
token whose revocation is an instant kill switch.
computers policies - Configuration and Business Policies (RMM-style), scope
trees validated at write time, actions checked against
the endpoints command allowlist. Feature-gated: every
route 404s while the setting is off, so enabling it can
never half-expose the surface.
asset CSV import - templates, a collector script carrying the site path, and
a validate-then-commit import. A plugin can own a virtual
column plus a post-write row hook, which is how a PC's
machine number becomes a controls link without a
hand-built relationships sheet.
agent/ - the Windows agent and its installer.
Contract 0.21.0 -> 0.24.0, additive throughout: register_import_hook (0.22),
mint/list/revoke_service_token (0.23), collector_authorized (0.24). The stability
table had no rows for any of them and now does.
MySQL 5.6 portability. The branch stored three policy columns as sa.JSON, the
only JSON columns in the product, and 5.6 has no JSON type: the migration failed
with a bare syntax error near the word JSON. They are TEXT now, through a
JSONText type decorator, so all seventeen call sites still read and write dicts.
The convention elsewhere is already TEXT (guides.blocksjson,
geenforce.manifestjson).
Review fixes carried on top:
- six policy GETs were jwt_required(optional=True), which authenticates a
caller without checking anything; they require computers.view now, and six
redundant jwt_required decorators are gone
- the agent installer took a default BaseUrl and CollectorKey, so a mistyped
run silently pointed an endpoint at the wrong instance; both are mandatory
and a non-https BaseUrl throws unless -AllowInsecureHttp is passed
- a committing CSV import wrote no audit row
Documentation: the 34 new routes are described in docs/api-inventory.json, which
is hand-maintained on purpose, and the generated spec now covers 331 paths and
477 operations across 22 surfaces. DEVELOPMENT-SETUP's 1071 troubleshooting row
said "use MySQL 8 for dev" and now gives the actual fix, including the trap that
5.6 downgrades ROW_FORMAT=DYNAMIC to Compact with a warning instead of an error.
Full suite 2123 passed, vitest 260 passed, all seventeen plugin chains migrate
clean on MySQL 5.6 and the naming, docs, parity and read-surface gates are green.
This commit is contained in:
@@ -273,6 +273,38 @@
|
||||
"params": "path: filename",
|
||||
"purpose": "Serve an uploaded branding logo (public - kiosks and print pages read it)",
|
||||
"example": "curl -O http://localhost:5001/api/settings/branding/logo-site.svg"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/settings/asset-import/script",
|
||||
"auth": "admin role",
|
||||
"params": "none",
|
||||
"purpose": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing before it runs",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/script -o Collect-ShopDB.ps1"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/settings/asset-import/templates",
|
||||
"auth": "admin role",
|
||||
"params": "none",
|
||||
"purpose": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/settings/asset-import/templates/<tablename>",
|
||||
"auth": "admin role",
|
||||
"params": "path: tablename",
|
||||
"purpose": "Download a generated CSV template for one importable table, with its real column names as the header row",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates/assets -o assets.csv"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/settings/asset-import/import",
|
||||
"auth": "admin role",
|
||||
"params": "multipart: one file per table; form: commit (false = validate only)",
|
||||
"purpose": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change; a committing run writes an AuditLog row with the reconciled count",
|
||||
"example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F \"assets=@assets.csv\" -F \"commit=true\" http://localhost:5001/api/settings/asset-import/import"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1729,6 +1761,102 @@
|
||||
"params": "none",
|
||||
"purpose": "The collector reporter (Report-AssetToShopDB.ps1) stamped with THIS site's values: the site base URL becomes the -ApiUrl default and computers_routableranges becomes -AllowedRanges, so it downloads ready to deploy. Only the parameter DEFAULTS are substituted - the copy in plugins/computers/client/ stays runnable, so there is no second version to drift - and everything stamped stays overridable by argument or registry. The collector key is deliberately NOT included: this file lands on every shop-floor PC, and a token spread across hundreds of bays cannot be rotated quietly. Serves text/plain as an attachment, with the SHA-256 in X-Script-Sha256 so a deployment can verify what it fetched",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/client-script"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/configuration",
|
||||
"auth": "computers.view",
|
||||
"params": "none",
|
||||
"purpose": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/configuration/<policy_id>",
|
||||
"auth": "computers.view",
|
||||
"params": "path: policy_id",
|
||||
"purpose": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/computers/policies/configuration",
|
||||
"auth": "computers.create",
|
||||
"params": "body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}",
|
||||
"purpose": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the endpoints command allowlist, so a bad tree or unknown action is rejected before it reaches the database; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"name\":\"Shopfloor MTConnect\",\"actions\":[{\"action\":\"mtconnect_start\",\"mode\":\"desired\"}]}' http://localhost:5001/api/computers/policies/configuration"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/computers/policies/configuration/<policy_id>",
|
||||
"auth": "computers.edit",
|
||||
"params": "path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)",
|
||||
"purpose": "Update a Configuration Policy; same scope and action validation as create; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/computers/policies/configuration/<policy_id>",
|
||||
"auth": "computers.delete",
|
||||
"params": "path: policy_id",
|
||||
"purpose": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/business",
|
||||
"auth": "computers.view",
|
||||
"params": "none",
|
||||
"purpose": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/business/<policy_id>",
|
||||
"auth": "computers.view",
|
||||
"params": "path: policy_id",
|
||||
"purpose": "One Business Policy with its member Configuration Policies in sort order; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/computers/policies/business",
|
||||
"auth": "computers.create",
|
||||
"params": "body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}",
|
||||
"purpose": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two match the same endpoint; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/computers/policies/business/<policy_id>",
|
||||
"auth": "computers.edit",
|
||||
"params": "path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)",
|
||||
"purpose": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/computers/policies/business/<policy_id>",
|
||||
"auth": "computers.delete",
|
||||
"params": "path: policy_id",
|
||||
"purpose": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/actions",
|
||||
"auth": "computers.view",
|
||||
"params": "none",
|
||||
"purpose": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The UI builds its action picker from this rather than hardcoding a list that would drift; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/actions"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/computers/policies/effective/<asset_id>",
|
||||
"auth": "computers.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies matched, and the resulting desired/gate actions. Read-only, for debugging a scope that is not matching what its author expected; 404 while the computers_configpolicies_enabled setting is off",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/effective/1asset_id"
|
||||
}
|
||||
],
|
||||
"surface": "plugin-computers"
|
||||
@@ -3623,5 +3751,154 @@
|
||||
"example": "curl http://localhost:5001/api/guides/images/5 -H \"Authorization: Bearer $TOK\" --output shot.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"surface": "plugin-endpoints",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/",
|
||||
"auth": "endpoints.view",
|
||||
"params": "query: search (hostname substring)",
|
||||
"purpose": "List endpoints reporting through the ShopDB agent, with last-seen and online state",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/<asset_id>",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "One endpoint: its agent state, version, and last check-in",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/<asset_id>/events",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id; query: eventid (page from this id)",
|
||||
"purpose": "Agent event history for one endpoint, newest first",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/events"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/asset/<asset_id>/status",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Asset-detail panel: online state, uptime, and agent version for one endpoint",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/status"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/asset/<asset_id>/telemetrymeta",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/telemetrymeta"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/asset/<asset_id>/services",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/services"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/asset/<asset_id>/recentevents",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Asset-detail panel: the most recent agent events for one endpoint",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/recentevents"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/asset/<asset_id>/interfaces",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/interfaces"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/dashboard/summary",
|
||||
"auth": "endpoints.view",
|
||||
"params": "none",
|
||||
"purpose": "Dashboard card: endpoint counts by online/offline/never-seen",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/summary"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/dashboard/quiet",
|
||||
"auth": "endpoints.view",
|
||||
"params": "none",
|
||||
"purpose": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/quiet"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/dashboard/stability",
|
||||
"auth": "endpoints.view",
|
||||
"params": "none",
|
||||
"purpose": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/stability"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/agentkey",
|
||||
"auth": "admin role",
|
||||
"params": "none",
|
||||
"purpose": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key itself",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/endpoints/agentkey/regenerate",
|
||||
"auth": "admin role",
|
||||
"params": "none",
|
||||
"purpose": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating it cannot disturb the computers or GE-Enforce collectors. Every agent must be reconfigured with the new value",
|
||||
"example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey/regenerate"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/endpoints/agentkey",
|
||||
"auth": "admin role",
|
||||
"params": "none",
|
||||
"purpose": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized immediately",
|
||||
"example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/<asset_id>/commands",
|
||||
"auth": "endpoints.view",
|
||||
"params": "path: asset_id",
|
||||
"purpose": "Command history for one endpoint: what was queued, claimed, and what it returned",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/commands"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/endpoints/<asset_id>/commands",
|
||||
"auth": "endpoints.command",
|
||||
"params": "path: asset_id; body: action (required, allowlisted), target, targetkind",
|
||||
"purpose": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a stricter permission, the effective policy must permit the action for this endpoint, and a second command will not stack while one is pending or running",
|
||||
"example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"action\":\"service_restart\",\"target\":\"MTConnectAgent\",\"targetkind\":\"service\"}' http://localhost:5001/api/endpoints/1/commands"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/endpoints/commands/next",
|
||||
"auth": "collector token (collector.ingest.endpoints)",
|
||||
"params": "query: hostname (required)",
|
||||
"purpose": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second poll claims it; 204 when nothing is queued inside the hold window, and the agent re-polls. Collector-token auth, not JWT",
|
||||
"example": "curl -H \"X-Collector-Key: $KEY\" \"http://localhost:5001/api/endpoints/commands/next?hostname=WJSF-PC-01\""
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/endpoints/commands/<command_id>/result",
|
||||
"auth": "collector token (collector.ingest.endpoints)",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user