Skip to content

Commit 15cf0ee

Browse files
authored
Merge pull request #15 from BenCodez/codex/control-plugin-deployment
Stage VotingPlugin updates from Control
2 parents 51a19e7 + 86150fa commit 15cf0ee

15 files changed

Lines changed: 2366 additions & 27 deletions

File tree

‎AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ The CI definition is `.github/workflows/maven.yml`. The shaded runnable artifact
107107
inspection gating on `VoteLogging.Enabled`, document that a restart is required after either toggle, and do not claim
108108
enabled/available/readable are interchangeable states. Serialize APPLY operations and retries that share a target so
109109
retained creation order is also successful completion order for restart-session warnings.
110+
14. `plugin.deploy.v1` stages only a verified VotingPlugin JAR for the next process restart. Keep uploads and expanded ZIP
111+
content bounded, content-addressed, private, symlink-safe, and atomically published. A node download requires its exact
112+
live session, attempt, and unexpired lease; each node independently verifies the hash and plugin identity. Never hot
113+
reload, automatically restart, or include older nodes that did not negotiate the exact capability. A Control restart
114+
invalidates in-progress download authority and requires an explicit retry.
110115

111116
## Paired protocol workflow
112117

‎README.md‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
VotingPlugin Control is a separate, local-first administration service for a VotingPlugin network. It provides
44
authenticated discovery of multiple BungeeCord and Velocity proxies, direct Bukkit backend enrollment, full VotingPlugin
5-
YAML configuration control, guided setup, redacted snapshots/drift comparison, durable operation history, and typed
6-
read-only vote/data diagnostics. The local WebUI uses the same versioned API. Control does not process votes, and
5+
YAML configuration control, guided setup, redacted snapshots/drift comparison, durable operation history, verified
6+
next-restart VotingPlugin JAR staging, and typed read-only vote/data diagnostics. The local WebUI uses the same versioned API. Control does not process votes, and
77
VotingPlugin does not depend on it for startup, joins, routing, rewards, or shutdown.
88

99
Maintainers and coding agents should read [AGENTS.md](AGENTS.md). The complete management, inspection, limits, and threat
@@ -55,8 +55,10 @@ report `development` rather than maintaining a second version literal.
5555
| `CONTROL_LAUNCH_ID` | empty | Optional UUID echoed by health checks so a supervising VotingPlugin can verify ownership of the listener |
5656
| `CONTROL_PARENT_PID` | empty | Optional supervising VotingPlugin process ID; hosted Control exits when that parent process ends |
5757

58-
The server also uses a bounded HTTP executor (8 active requests and a 32-request queue), a 4 MiB request limit, bounded
59-
JSON depth/string/number sizes, a two-worker password-verification executor with per-client admission, and a bounded invalid-authentication failure limit. Configure the exact reverse-proxy IP addresses when HTTPS terminates upstream; forwarding headers from every other peer are ignored. Valid enrolled/admin credentials remain
58+
The server also uses a bounded HTTP executor (8 active requests and a 32-request queue), a 4 MiB limit for generic JSON
59+
requests, and a separate bounded streaming artifact-upload route with a 64 MiB maximum. JSON depth/string/number sizes
60+
are bounded, password verification uses two workers with per-client admission, and invalid-authentication failures are
61+
bounded. Configure the exact reverse-proxy IP addresses when HTTPS terminates upstream; forwarding headers from every other peer are ignored. Valid enrolled/admin credentials remain
6062
usable even while invalid traffic is throttled. Shutdown stops the server and its daemon request workers without waiting
6163
indefinitely.
6264

@@ -158,6 +160,13 @@ All errors have the stable form:
158160
| `POST` | `/api/v1/nodes/{nodeId}/inspections/{inspectionId}/result` | matching node | Complete that inspection attempt |
159161
| `GET`, `POST` | `/api/v1/snapshots` | admin or WebUI session; CSRF for POST | List summaries or save a named snapshot from a completed file read |
160162
| `GET` | `/api/v1/snapshots/{snapshotId}` | admin or WebUI session | Load one durable snapshot's full redacted file content |
163+
| `POST` | `/api/v1/artifacts/votingplugin` | admin or WebUI session + CSRF | Stream and verify one bounded VotingPlugin JAR into private content-addressed storage |
164+
| `GET`, `POST` | `/api/v1/deployments` | admin or WebUI session; CSRF for POST | List deployment history or stage a verified artifact on explicit `plugin.deploy.v1` nodes |
165+
| `GET` | `/api/v1/deployments/{deploymentId}` | admin or WebUI session | Read durable per-node staging state |
166+
| `POST` | `/api/v1/deployments/{deploymentId}/retry` | admin or WebUI session + CSRF | Retry only failed, currently eligible nodes as a new operation |
167+
| `POST` | `/api/v1/nodes/{nodeId}/deployments` | matching node | Claim one session-pinned deployment task, or `204` |
168+
| `GET` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact` | matching node + exact session/attempt headers | Download the claimed verified JAR during its lease |
169+
| `POST` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/result` | matching node | Complete the exact staging attempt |
161170

162171
Routes are exact. Child suffixes do not inherit a handler, every known endpoint has an intentional method/structured 405,
163172
and all unknown endpoints return a structured 404.
@@ -197,9 +206,16 @@ Control and VotingPlugin both enforce fixed quick-setup preset/option schemas; u
197206
than becoming arbitrary YAML writes. The WebUI settings catalog is a static versioned reference over these typed paths,
198207
not a generic setting API.
199208

200-
Read actions load only the primary server shown in the configuration header. Preview and apply still cover every server
209+
Opening Settings or changing the selected server automatically reads that server's current configuration. A failed read
210+
clears the editor and exposes an inline retry; successful applies invalidate cached values and read the confirmed state
211+
again. Read actions load only the primary server shown in the configuration header. Preview and apply still cover every server
201212
explicitly included in configuration changes, so one slow secondary node does not delay opening the editor or guided form.
202213

214+
`plugin.deploy.v1` is additive and exact: older nodes remain connected but are excluded from JAR staging. The WebUI uploads
215+
at most 64 MiB, Control validates the ZIP structure and root `plugin.yml`, and every node re-verifies the SHA-256 before
216+
staging. Staging never reloads or restarts a server; success is reported as `RESTART_REQUIRED`. Interrupted Control attempts
217+
become failed durable history and require an explicit retry, preventing a pre-restart lease from authorizing a download.
218+
203219
`data.inspect.v1` is a separate read-only lane for overview, vote-site health (including persisted unconfigured service
204220
observations), exact-player data, bounded VoteLog summary/search/correlation trace, non-creating service-site resolution,
205221
no-side-effect reward simulation, and redacted diagnostics. It accepts only allow-listed string filters and bounded result

‎docs/control-management.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ when Control is stopped.
77

88
## Mental model
99

10-
There are three independent lanes:
10+
There are four independent lanes:
1111

1212
| Lane | Capability examples | Direction | Can mutate a node? | Persistence |
1313
| --- | --- | --- | --- | --- |
1414
| Discovery | `discovery.read`, `presence.snapshot` | Node pushes registration/heartbeat/presence | No | Current topology is in memory |
1515
| Configuration | `config.files.v1`, `config.quick-setup.v1` | Browser queues; node polls and reports | Only after preview and approval | Redacted history and audit are durable; live task input is in memory |
1616
| Inspection | `data.inspect.v1` | Browser queues; Bukkit node polls and reports | Never | Short-lived result is in memory; kind-only audit is durable |
17+
| Deployment | `plugin.deploy.v1` | Browser queues; node polls and downloads under its exact session and attempt lease | Stages a verified JAR only; never reloads or restarts | Artifacts and deployment history are durable |
1718

1819
Connectors always initiate outbound HTTP(S) to Control. No Control feature adds an inbound port to a Minecraft process.
1920
One node credential is bound to one stable node ID. Browser sessions and the API automation credential are separate from
@@ -47,6 +48,7 @@ Control accepts only the intersection with its own allow-list.
4748
| `config.transport-test.v1` | Typed, bounded proxy-to-backend communication check |
4849
| `config.proxy-method.v1` | Coordinated preview/apply and acknowledged runtime replacement for a supported network proxy method |
4950
| `config.proxy-method.v2` | HTTP proxy-method selection and HTTP backend quick setup; only paired connectors implementing the HTTP contract advertise it |
51+
| `plugin.deploy.v1` | Verify and stage a VotingPlugin JAR for the node's next process restart; never hot reload or restart it |
5052
| `data.inspect.v1` | Typed read-only data, health, simulation, and diagnostics requests |
5153

5254
Do not infer support from plugin version strings. Check `acceptedCapabilities` for the exact capability.
@@ -64,7 +66,8 @@ authenticated, CSRF-protected endpoint and node capability checks as an external
6466
| Overview dashboard | Builds six summary cards, Attention Required, quick actions, topology, logged-service activity, and recent operation activity from the node registry plus existing `overview`, `vote-site-health`, and `vote-log-summary` inspections | Read-only aggregation; failed, incomplete, or malformed sub-inspections produce a warning rather than a Healthy claim; disconnected registered nodes produce a warning; unknown Minecraft presence is not treated as offline; VoteLog counts are labeled as logged events; actions only open existing safe workflows rather than inventing auto-fixes |
6567
| Network Doctor | Runs `diagnostics` (which includes the overview fields), combines node health with Control's current topology, and displays checks for connector, configuration, Votifier, vote sites, rewards, logging, and proxy topology | Read-only; “healthy” is bounded reported state, not a synthetic vote |
6668
| Diagnostics download | Downloads the last Network Doctor result as local JSON | Redacted status bundle only; no raw configuration/logs/player records/infrastructure secrets |
67-
| Activity | Loads the newest 50 live/recovered operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
69+
| Activity | Loads the newest 50 live/recovered configuration operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered configuration history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
70+
| Plugin update | Uploads one bounded JAR, shows the deployment-capable subset, and stages it on those nodes | SHA-256 and JAR identity are verified; session/attempt leases authorize downloads; Control never automatically reloads or restarts nodes; private storage is capped at 32 artifacts / 512 MiB and evicts only artifacts not referenced by retained deployment history |
6871
| Fast file reads | Caches a successful file read for 30 seconds by node ID, node session, and file | Browser memory only; cleared on logout and successful relevant writes; session binding prevents reuse after reconnect |
6972
| Full-YAML drafts | Keeps unsaved editor contents during a registry refresh | A dirty draft is bound to its source node, session, and file; it cannot preview or apply after that session changes. The operator must explicitly confirm a current-file read/reload, which discards the retained draft and rebinds the editor. |
7073
| Proxy configuration | Opens `bungeeconfig.yml` only for the selected online proxy that negotiated `config.proxy-files.v1` | Fixed one-file capability, never proxy file browsing; redacted READ, PREVIEW, and one-time approved APPLY still apply |
@@ -81,7 +84,11 @@ authenticated, CSRF-protected endpoint and node capability checks as an external
8184

8285
The Setup tab replaces the former “Quick Setup” framing but retains existing typed presets, VoteSites sync, detected-plugin
8386
command suggestions, preview, approval, node backup, reload, and rollback. Setup profiles are convenience input only; live
84-
values should be loaded before modifying an existing configuration.
87+
values should be loaded before modifying an existing configuration. The normal Settings flow performs that read
88+
automatically on entry and whenever the selected node or file changes. It clears the prior node's editable state before the
89+
request, deduplicates navigation-triggered reads, caches successful reads briefly by node session and file, and
90+
invalidates/re-reads after a successful apply. Failure leaves no stale editable value and exposes an inline Retry action.
91+
None of these reads can trigger APPLY.
8592

8693
## Configuration operations
8794

@@ -450,6 +457,13 @@ node resources require the bearer credential bound to the path node ID.
450457
| `POST` | `/api/v1/operations/{operationId}/retry` | admin/browser + CSRF | Reissue safe failed work as a new operation |
451458
| `POST` | `/api/v1/nodes/{nodeId}/operations` | matching node | Claim one configuration task or `204` |
452459
| `POST` | `/api/v1/nodes/{nodeId}/operations/{operationId}/result` | matching node | Complete one claimed configuration task |
460+
| `POST` | `/api/v1/artifacts/votingplugin` | admin/browser + CSRF | Stream, hash, inspect, and atomically retain one VotingPlugin JAR (64 MiB maximum) |
461+
| `GET`, `POST` | `/api/v1/deployments` | admin/browser; CSRF for POST | List durable staging operations or target an exact verified artifact |
462+
| `GET` | `/api/v1/deployments/{deploymentId}` | admin/browser | Read per-node staging state |
463+
| `POST` | `/api/v1/deployments/{deploymentId}/retry` | admin/browser + CSRF | Create a new operation for currently eligible failed targets only |
464+
| `POST` | `/api/v1/nodes/{nodeId}/deployments` | matching node | Claim one session-pinned deployment task |
465+
| `GET` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact` | matching node + session/attempt headers | Stream the verified artifact during the exact live lease |
466+
| `POST` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/result` | matching node | Complete the exact attempt with staged/restart-required or a bounded failure |
453467
| `POST` | `/api/v1/inspections` | admin/browser + CSRF | Queue one typed read-only query |
454468
| `GET` | `/api/v1/inspections/{inspectionId}` | admin/browser | Read short-lived inspection status/result |
455469
| `POST` | `/api/v1/nodes/{nodeId}/inspections` | matching node | Claim one inspection or `204` |
@@ -471,7 +485,8 @@ tunnel/network outside loopback.
471485

472486
| Boundary | Limit/behavior |
473487
| --- | --- |
474-
| HTTP request | 4 MiB; bounded Jackson depth/string/number constraints; duplicate and trailing JSON rejected |
488+
| Generic JSON request | 4 MiB; bounded Jackson depth/string/number constraints; duplicate and trailing JSON rejected |
489+
| VotingPlugin artifact upload | Separate bounded streaming route; 64 MiB maximum |
475490
| HTTP execution | 8 active request workers plus queue of 32; bounded request/response time |
476491
| Browser sessions | 100; 30-minute idle and 8-hour absolute expiry; HttpOnly, SameSite=Strict cookie |
477492
| Node operation targets | 1–100 distinct online capable nodes |

‎src/main/java/com/bencodez/votingplugin/control/ControlApplication.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.bencodez.votingplugin.control;
22

33
import com.bencodez.votingplugin.control.auth.CredentialStore;
4+
import com.bencodez.votingplugin.control.artifact.ArtifactStore;
45
import com.bencodez.votingplugin.control.domain.InMemoryNodeRegistry;
56
import com.bencodez.votingplugin.control.domain.ConfigurationAuditLog;
67
import com.bencodez.votingplugin.control.domain.ConfigurationOperations;
78
import com.bencodez.votingplugin.control.domain.ConfigurationOperationJournal;
89
import com.bencodez.votingplugin.control.domain.ConfigurationSnapshots;
910
import com.bencodez.votingplugin.control.domain.InspectionOperations;
11+
import com.bencodez.votingplugin.control.domain.DeploymentOperations;
1012
import com.bencodez.votingplugin.control.http.ControlHttpServer;
1113
import com.bencodez.votingplugin.control.protocol.ControlIdentity;
1214
import com.bencodez.votingplugin.control.protocol.Protocol;
@@ -162,9 +164,12 @@ static void runServer(Map<String, String> environment) throws Exception {
162164
ConfigurationOperations operations = new ConfigurationOperations(registry, audit, clock, operationJournal);
163165
InspectionOperations inspections = new InspectionOperations(registry, audit, clock);
164166
ConfigurationSnapshots snapshots = new ConfigurationSnapshots(configuration.dataDirectory(), clock);
167+
ArtifactStore artifacts = new ArtifactStore(configuration.dataDirectory().resolve("plugin-artifacts"));
168+
DeploymentOperations deployments = new DeploymentOperations(registry, audit,
169+
configuration.dataDirectory(), clock);
165170
ControlHttpServer server = new ControlHttpServer(configuration.address(), registry, identity, credentials,
166-
operations, inspections, snapshots, configuration.secureCookies(), configuration.trustedProxyAddresses(),
167-
configuration.launchId());
171+
operations, inspections, snapshots, artifacts, deployments, configuration.secureCookies(),
172+
configuration.trustedProxyAddresses(), configuration.launchId());
168173
ProcessHandle parent = parentProcess(configuration.parentPid());
169174
CountDownLatch shutdown = new CountDownLatch(1);
170175
Runtime.getRuntime().addShutdownHook(new Thread(() -> {

0 commit comments

Comments
 (0)